├── .circleci └── config.yml ├── .gitignore ├── .npmignore ├── LICENSE.md ├── README.md ├── docs ├── assets │ ├── css │ │ └── main.css │ ├── images │ │ ├── icons.png │ │ ├── icons@2x.png │ │ ├── widgets.png │ │ └── widgets@2x.png │ └── js │ │ ├── main.js │ │ └── search.json ├── classes │ └── jsonpath.html ├── globals.html └── index.html ├── fixtures └── store.json ├── generate-parser.ts ├── generated └── parser.js ├── include ├── action.js └── module.js ├── package-lock.json ├── package.json ├── src ├── assert.ts ├── grammar.ts ├── handlers.ts ├── index.ts ├── jsonpath.ts ├── lessons.test.ts ├── parse.test.ts ├── parser.ts ├── query.test.ts ├── slice.test.ts ├── slice.ts ├── stringify.test.ts ├── sugar.test.ts ├── test.ts └── tokens.ts ├── tsconfig.json └── upstream └── aesprim.js /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | build: 4 | docker: 5 | - image: circleci/node:7.10 6 | working_directory: ~/repo 7 | steps: 8 | - checkout 9 | - restore_cache: 10 | keys: 11 | - v1-dependencies-{{ checksum "package.json" }} 12 | - v1-dependencies- 13 | - run: npm install 14 | - save_cache: 15 | paths: 16 | - node_modules 17 | key: v1-dependencies-{{ checksum "package.json" }} 18 | - run: npm test 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .circleci/ 2 | fixtures/ 3 | dist/test.js 4 | dist/**/*.test.js 5 | node_modules/ 6 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014-2016 David Chester 2 | Copyright (c) 2020-2021 Astronaut Labs, LLC. 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the Software without restriction, including without limitation the 6 | rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit 7 | persons to whom the Software is furnished to do so, subject to the following conditions: 8 | 9 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the 10 | Software. 11 | 12 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE 13 | WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 14 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 15 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # @/jsonpath [![CircleCI](https://circleci.com/gh/astronautlabs/jsonpath.svg?style=svg)](https://circleci.com/gh/astronautlabs/jsonpath) 2 | 3 | [![npm (scoped)](https://img.shields.io/npm/v/@astronautlabs/jsonpath?style=flat-square)](https://npmjs.com/package/@astronautlabs/jsonpath) 4 | 5 | Query JavaScript objects with JSONPath expressions. 6 | Robust / safe JSONPath engine for Node.js. 7 | 8 | 9 | ## Query Example 10 | 11 | ```javascript 12 | import { JSONPath } from '@astronautlabs/jsonpath'; 13 | 14 | let cities = [ 15 | { name: "London", "population": 8615246 }, 16 | { name: "Berlin", "population": 3517424 }, 17 | { name: "Madrid", "population": 3165235 }, 18 | { name: "Rome", "population": 2870528 } 19 | ]; 20 | 21 | let names = JSONPath.query(cities, '$..name'); 22 | 23 | // [ "London", "Berlin", "Madrid", "Rome" ] 24 | ``` 25 | 26 | ## Install 27 | 28 | Install from npm: 29 | ```bash 30 | $ npm install @astronautlabs/jsonpath 31 | ``` 32 | 33 | ## JSONPath Syntax 34 | 35 | Here are syntax and examples adapted from [Stefan Goessner's original post](http://goessner.net/articles/JsonPath/) introducing JSONPath in 2007. 36 | 37 | JSONPath | Description 38 | -----------------|------------ 39 | `$` | The root object/element 40 | `@` | The current object/element 41 | `.` | Child member operator 42 | `..` | Recursive descendant operator; JSONPath borrows this syntax from E4X 43 | `*` | Wildcard matching all objects/elements regardless their names 44 | `[]` | Subscript operator 45 | `[,]` | Union operator for alternate names or array indices as a set 46 | `[start:end:step]` | Array slice operator borrowed from ES4 / Python 47 | `?()` | Applies a filter (script) expression via static evaluation 48 | `()` | Script expression via static evaluation 49 | 50 | Given this sample data set, see example expressions below: 51 | 52 | ```javascript 53 | { 54 | "store": { 55 | "book": [ 56 | { 57 | "category": "reference", 58 | "author": "Nigel Rees", 59 | "title": "Sayings of the Century", 60 | "price": 8.95 61 | }, { 62 | "category": "fiction", 63 | "author": "Evelyn Waugh", 64 | "title": "Sword of Honour", 65 | "price": 12.99 66 | }, { 67 | "category": "fiction", 68 | "author": "Herman Melville", 69 | "title": "Moby Dick", 70 | "isbn": "0-553-21311-3", 71 | "price": 8.99 72 | }, { 73 | "category": "fiction", 74 | "author": "J. R. R. Tolkien", 75 | "title": "The Lord of the Rings", 76 | "isbn": "0-395-19395-8", 77 | "price": 22.99 78 | } 79 | ], 80 | "bicycle": { 81 | "color": "red", 82 | "price": 19.95 83 | } 84 | } 85 | } 86 | ``` 87 | 88 | Example JSONPath expressions: 89 | 90 | JSONPath | Description 91 | ------------------------------|------------ 92 | `$.store.book[*].author` | The authors of all books in the store 93 | `$..author` | All authors 94 | `$.store.*` | All things in store, which are some books and a red bicycle 95 | `$.store..price` | The price of everything in the store 96 | `$..book[2]` | The third book 97 | `$..book[(@.length-1)]` | The last book via script subscript 98 | `$..book[-1:]` | The last book via slice 99 | `$..book[0,1]` | The first two books via subscript union 100 | `$..book[:2]` | The first two books via subscript array slice 101 | `$..book[?(@.isbn)]` | Filter all books with isbn number 102 | `$..book[?(@.price<10)]` | Filter all books cheaper than 10 103 | `$..book[?(@.price==8.95)]` | Filter all books that cost 8.95 104 | `$..book[?(@.price<30 && @.category=="fiction")]` | Filter all fiction books cheaper than 30 105 | `$..*` | All members of JSON structure 106 | 107 | 108 | ## Methods 109 | 110 | #### JSONPath.query(obj, pathExpression[, count]) 111 | 112 | Find elements in `obj` matching `pathExpression`. Returns an array of elements that satisfy the provided JSONPath expression, or an empty array if none were matched. Returns only first `count` elements if specified. 113 | 114 | ```javascript 115 | let authors = jp.query(data, '$..author'); 116 | // [ 'Nigel Rees', 'Evelyn Waugh', 'Herman Melville', 'J. R. R. Tolkien' ] 117 | ``` 118 | 119 | #### JSONPath.paths(obj, pathExpression[, count]) 120 | 121 | Find paths to elements in `obj` matching `pathExpression`. Returns an array of element paths that satisfy the provided JSONPath expression. Each path is itself an array of keys representing the location within `obj` of the matching element. Returns only first `count` paths if specified. 122 | 123 | 124 | ```javascript 125 | let paths = jp.paths(data, '$..author'); 126 | // [ 127 | // ['$', 'store', 'book', 0, 'author'] }, 128 | // ['$', 'store', 'book', 1, 'author'] }, 129 | // ['$', 'store', 'book', 2, 'author'] }, 130 | // ['$', 'store', 'book', 3, 'author'] } 131 | // ] 132 | ``` 133 | 134 | #### JSONPath.nodes(obj, pathExpression[, count]) 135 | 136 | Find elements and their corresponding paths in `obj` matching `pathExpression`. Returns an array of node objects where each node has a `path` containing an array of keys representing the location within `obj`, and a `value` pointing to the matched element. Returns only first `count` nodes if specified. 137 | 138 | ```javascript 139 | let nodes = jp.nodes(data, '$..author'); 140 | // [ 141 | // { path: ['$', 'store', 'book', 0, 'author'], value: 'Nigel Rees' }, 142 | // { path: ['$', 'store', 'book', 1, 'author'], value: 'Evelyn Waugh' }, 143 | // { path: ['$', 'store', 'book', 2, 'author'], value: 'Herman Melville' }, 144 | // { path: ['$', 'store', 'book', 3, 'author'], value: 'J. R. R. Tolkien' } 145 | // ] 146 | ``` 147 | 148 | #### JSONPath.value(obj, pathExpression[, newValue]) 149 | 150 | Returns the value of the first element matching `pathExpression`. If `newValue` is provided, sets the value of the first matching element and returns the new value. 151 | 152 | #### JSONPath.parent(obj, pathExpression) 153 | 154 | Returns the parent of the first matching element. 155 | 156 | #### JSONPath.apply(obj, pathExpression, fn) 157 | 158 | Runs the supplied function `fn` on each matching element, and replaces each matching element with the return value from the function. The function accepts the value of the matching element as its only parameter. Returns matching nodes with their updated values. 159 | 160 | 161 | ```javascript 162 | let nodes = jp.apply(data, '$..author', function(value) { return value.toUpperCase() }); 163 | // [ 164 | // { path: ['$', 'store', 'book', 0, 'author'], value: 'NIGEL REES' }, 165 | // { path: ['$', 'store', 'book', 1, 'author'], value: 'EVELYN WAUGH' }, 166 | // { path: ['$', 'store', 'book', 2, 'author'], value: 'HERMAN MELVILLE' }, 167 | // { path: ['$', 'store', 'book', 3, 'author'], value: 'J. R. R. TOLKIEN' } 168 | // ] 169 | ``` 170 | 171 | #### JSONPath.parse(pathExpression) 172 | 173 | Parse the provided JSONPath expression into path components and their associated operations. 174 | 175 | ```javascript 176 | let path = jp.parse('$..author'); 177 | // [ 178 | // { expression: { type: 'root', value: '$' } }, 179 | // { expression: { type: 'identifier', value: 'author' }, operation: 'member', scope: 'descendant' } 180 | // ] 181 | ``` 182 | 183 | #### JSONPath.stringify(path) 184 | 185 | Returns a path expression in string form, given a path. The supplied path may either be a flat array of keys, as returned by `jp.nodes` for example, or may alternatively be a fully parsed path expression in the form of an array of path components as returned by `jp.parse`. 186 | 187 | ```javascript 188 | let pathExpression = jp.stringify(['$', 'store', 'book', 0, 'author']); 189 | // "$.store.book[0].author" 190 | ``` 191 | 192 | ## Differences from Original Implementation 193 | 194 | This implementation aims to be compatible with Stefan Goessner's original implementation with a few notable exceptions described below. 195 | 196 | #### Evaluating Script Expressions 197 | 198 | Script expressions (i.e, `(...)` and `?(...)`) are statically evaluated via [static-eval](https://github.com/substack/static-eval) rather than using the underlying script engine directly. That means both that the scope is limited to the instance variable (`@`), and only simple expressions (with no side effects) will be valid. So for example, `?(@.length>10)` will be just fine to match arrays with more than ten elements, but `?(process.exit())` will not get evaluated since `process` would yield a `ReferenceError`. This method is even safer than `vm.runInNewContext`, since the script engine itself is more limited and entirely distinct from the one running the application code. See more details in the [implementation](https://github.com/substack/static-eval/blob/master/index.js) of the evaluator. 199 | 200 | #### Grammar 201 | 202 | This project uses a formal BNF [grammar](https://github.com/dchester/jsonpath/blob/master/lib/grammar.js) to parse JSONPath expressions, an attempt at reverse-engineering the intent of the original implementation, which parses via a series of creative regular expressions. The original regex approach can sometimes be forgiving for better or for worse (e.g., `$['store]` => `$['store']`), and in other cases, can be just plain wrong (e.g. `[` => `$`). 203 | 204 | #### Other Minor Differences 205 | 206 | As a result of using a real parser and static evaluation, there are some arguable bugs in the original library that have not been carried through here: 207 | 208 | - strings in subscripts may now be double-quoted 209 | - final `step` arguments in slice operators may now be negative 210 | - script expressions may now contain `.` and `@` characters not referring to instance variables 211 | - subscripts no longer act as character slices on string elements 212 | - non-ascii non-word characters are no-longer valid in member identifier names; use quoted subscript strings instead (e.g., `$['$']` instead of `$.$`) 213 | - unions now yield real unions with no duplicates rather than concatenated results 214 | 215 | ## License 216 | 217 | [MIT](LICENSE) 218 | 219 | -------------------------------------------------------------------------------- /docs/assets/images/icons.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/astronautlabs/jsonpath/d391fbe2db68780591efb2a573be3432c9c4a4b4/docs/assets/images/icons.png -------------------------------------------------------------------------------- /docs/assets/images/icons@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/astronautlabs/jsonpath/d391fbe2db68780591efb2a573be3432c9c4a4b4/docs/assets/images/icons@2x.png -------------------------------------------------------------------------------- /docs/assets/images/widgets.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/astronautlabs/jsonpath/d391fbe2db68780591efb2a573be3432c9c4a4b4/docs/assets/images/widgets.png -------------------------------------------------------------------------------- /docs/assets/images/widgets@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/astronautlabs/jsonpath/d391fbe2db68780591efb2a573be3432c9c4a4b4/docs/assets/images/widgets@2x.png -------------------------------------------------------------------------------- /docs/assets/js/main.js: -------------------------------------------------------------------------------- 1 | !function(){var e=function(t){var r=new e.Builder;return r.pipeline.add(e.trimmer,e.stopWordFilter,e.stemmer),r.searchPipeline.add(e.stemmer),t.call(r,r),r.build()};e.version="2.3.7",e.utils={},e.utils.warn=function(e){return function(t){e.console&&console.warn&&console.warn(t)}}(this),e.utils.asString=function(e){return null==e?"":e.toString()},e.utils.clone=function(e){if(null==e)return e;for(var t=Object.create(null),r=Object.keys(e),i=0;i=this.length)return e.QueryLexer.EOS;var t=this.str.charAt(this.pos);return this.pos+=1,t},e.QueryLexer.prototype.width=function(){return this.pos-this.start},e.QueryLexer.prototype.ignore=function(){this.start==this.pos&&(this.pos+=1),this.start=this.pos},e.QueryLexer.prototype.backup=function(){this.pos-=1},e.QueryLexer.prototype.acceptDigitRun=function(){for(var t,r;47<(r=(t=this.next()).charCodeAt(0))&&r<58;);t!=e.QueryLexer.EOS&&this.backup()},e.QueryLexer.prototype.more=function(){return this.pos=this.scrollTop||0===this.scrollTop,isShown!==this.showToolbar&&(this.toolbar.classList.toggle("tsd-page-toolbar--hide"),this.secondaryNav.classList.toggle("tsd-navigation--toolbar-hide")),this.lastY=this.scrollTop},Viewport}(typedoc.EventTarget);typedoc.Viewport=Viewport,typedoc.registerService(Viewport,"viewport")}(typedoc||(typedoc={})),function(typedoc){function Component(options){this.el=options.el}typedoc.Component=Component}(typedoc||(typedoc={})),function(typedoc){typedoc.pointerDown="mousedown",typedoc.pointerMove="mousemove",typedoc.pointerUp="mouseup",typedoc.pointerDownPosition={x:0,y:0},typedoc.preventNextClick=!1,typedoc.isPointerDown=!1,typedoc.isPointerTouch=!1,typedoc.hasPointerMoved=!1,typedoc.isMobile=/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent),document.documentElement.classList.add(typedoc.isMobile?"is-mobile":"not-mobile"),typedoc.isMobile&&"ontouchstart"in document.documentElement&&(typedoc.isPointerTouch=!0,typedoc.pointerDown="touchstart",typedoc.pointerMove="touchmove",typedoc.pointerUp="touchend"),document.addEventListener(typedoc.pointerDown,function(e){typedoc.isPointerDown=!0,typedoc.hasPointerMoved=!1;var t="touchstart"==typedoc.pointerDown?e.targetTouches[0]:e;typedoc.pointerDownPosition.y=t.pageY||0,typedoc.pointerDownPosition.x=t.pageX||0}),document.addEventListener(typedoc.pointerMove,function(e){if(typedoc.isPointerDown&&!typedoc.hasPointerMoved){var t="touchstart"==typedoc.pointerDown?e.targetTouches[0]:e,x=typedoc.pointerDownPosition.x-(t.pageX||0),y=typedoc.pointerDownPosition.y-(t.pageY||0);typedoc.hasPointerMoved=10scrollTop;)index-=1;for(;index"+match+""}),parent=row.parent||"";(parent=parent.replace(new RegExp(this.query,"i"),function(match){return""+match+""}))&&(name=''+parent+"."+name);var item=document.createElement("li");item.classList.value=row.classes,item.innerHTML='\n '+name+"'\n ",this.results.appendChild(item)}}},Search.prototype.setLoadingState=function(value){this.loadingState!=value&&(this.el.classList.remove(SearchLoadingState[this.loadingState].toLowerCase()),this.loadingState=value,this.el.classList.add(SearchLoadingState[this.loadingState].toLowerCase()),this.updateResults())},Search.prototype.setHasFocus=function(value){this.hasFocus!=value&&(this.hasFocus=value,this.el.classList.toggle("has-focus"),value?(this.setQuery(""),this.field.value=""):this.field.value=this.query)},Search.prototype.setQuery=function(value){this.query=value.trim(),this.updateResults()},Search.prototype.setCurrentResult=function(dir){var current=this.results.querySelector(".current");if(current){var rel=1==dir?current.nextElementSibling:current.previousElementSibling;rel&&(current.classList.remove("current"),rel.classList.add("current"))}else(current=this.results.querySelector(1==dir?"li:first-child":"li:last-child"))&¤t.classList.add("current")},Search.prototype.gotoCurrentResult=function(){var current=this.results.querySelector(".current");if(current||(current=this.results.querySelector("li:first-child")),current){var link=current.querySelector("a");link&&(window.location.href=link.href),this.field.blur()}},Search.prototype.bindEvents=function(){var _this=this;this.results.addEventListener("mousedown",function(){_this.resultClicked=!0}),this.results.addEventListener("mouseup",function(){_this.resultClicked=!1,_this.setHasFocus(!1)}),this.field.addEventListener("focusin",function(){_this.setHasFocus(!0),_this.loadIndex()}),this.field.addEventListener("focusout",function(){_this.resultClicked?_this.resultClicked=!1:setTimeout(function(){return _this.setHasFocus(!1)},100)}),this.field.addEventListener("input",function(){_this.setQuery(_this.field.value)}),this.field.addEventListener("keydown",function(e){13==e.keyCode||27==e.keyCode||38==e.keyCode||40==e.keyCode?(_this.preventPress=!0,e.preventDefault(),13==e.keyCode?_this.gotoCurrentResult():27==e.keyCode?_this.field.blur():38==e.keyCode?_this.setCurrentResult(-1):40==e.keyCode&&_this.setCurrentResult(1)):_this.preventPress=!1}),this.field.addEventListener("keypress",function(e){_this.preventPress&&e.preventDefault()}),document.body.addEventListener("keydown",function(e){e.altKey||e.ctrlKey||e.metaKey||!_this.hasFocus&&47this.groups.length-1&&(index=this.groups.length-1),this.index!=index){var to=this.groups[index];if(-1 2 | 3 | 4 | 5 | 6 | JSONPath | @astronautlabs/jsonpath 7 | 8 | 9 | 10 | 11 | 12 |
13 |
14 |
15 |
16 | 27 |
28 |
29 | Options 30 |
31 |
32 | All 33 |
    34 |
  • Public
  • 35 |
  • Public/Protected
  • 36 |
  • All
  • 37 |
38 |
39 | 40 | 41 |
42 |
43 | Menu 44 |
45 |
46 |
47 |
48 |
49 |
50 | 58 |

Class JSONPath

59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |

Hierarchy

67 |
    68 |
  • 69 | JSONPath 70 |
  • 71 |
72 |
73 |
74 |

Index

75 |
76 |
77 |
78 |

Methods

79 | 89 |
90 |
91 |
92 |
93 |
94 |

Methods

95 |
96 | 97 |

Static apply

98 |
    99 |
  • apply(obj: any, string: any, fn: any): any[]
  • 100 |
101 |
    102 |
  • 103 | 108 |

    Parameters

    109 |
      110 |
    • 111 |
      obj: any
      112 |
    • 113 |
    • 114 |
      string: any
      115 |
    • 116 |
    • 117 |
      fn: any
      118 |
    • 119 |
    120 |

    Returns any[]

    121 |
  • 122 |
123 |
124 |
125 | 126 |

Static nodes

127 |
    128 |
  • nodes(obj: any, string: any, count?: any): any[]
  • 129 |
130 |
    131 |
  • 132 | 137 |

    Parameters

    138 |
      139 |
    • 140 |
      obj: any
      141 |
    • 142 |
    • 143 |
      string: any
      144 |
    • 145 |
    • 146 |
      Optional count: any
      147 |
    • 148 |
    149 |

    Returns any[]

    150 |
  • 151 |
152 |
153 |
154 | 155 |

Static parent

156 |
    157 |
  • parent(obj: any, string: any): any
  • 158 |
159 |
    160 |
  • 161 | 166 |

    Parameters

    167 |
      168 |
    • 169 |
      obj: any
      170 |
    • 171 |
    • 172 |
      string: any
      173 |
    • 174 |
    175 |

    Returns any

    176 |
  • 177 |
178 |
179 |
180 | 181 |

Static parse

182 |
    183 |
  • parse(string: string): JSONPath
  • 184 |
185 |
    186 |
  • 187 | 192 |

    Parameters

    193 |
      194 |
    • 195 |
      string: string
      196 |
    • 197 |
    198 |

    Returns JSONPath

    199 |
  • 200 |
201 |
202 |
203 | 204 |

Static paths

205 |
    206 |
  • paths(obj: any, string: any, count: any): any[]
  • 207 |
208 |
    209 |
  • 210 | 215 |

    Parameters

    216 |
      217 |
    • 218 |
      obj: any
      219 |
    • 220 |
    • 221 |
      string: any
      222 |
    • 223 |
    • 224 |
      count: any
      225 |
    • 226 |
    227 |

    Returns any[]

    228 |
  • 229 |
230 |
231 |
232 | 233 |

Static query

234 |
    235 |
  • query(obj: Object, string: any, count?: any): any[]
  • 236 |
237 |
    238 |
  • 239 | 244 |

    Parameters

    245 |
      246 |
    • 247 |
      obj: Object
      248 |
    • 249 |
    • 250 |
      string: any
      251 |
    • 252 |
    • 253 |
      Optional count: any
      254 |
    • 255 |
    256 |

    Returns any[]

    257 |
  • 258 |
259 |
260 |
261 | 262 |

Static stringify

263 |
    264 |
  • stringify(path: any): string
  • 265 |
266 |
    267 |
  • 268 | 273 |

    Parameters

    274 |
      275 |
    • 276 |
      path: any
      277 |
    • 278 |
    279 |

    Returns string

    280 |
  • 281 |
282 |
283 |
284 | 285 |

Static value

286 |
    287 |
  • value(obj: any, path: any, value?: any): any
  • 288 |
289 |
    290 |
  • 291 | 296 |

    Parameters

    297 |
      298 |
    • 299 |
      obj: any
      300 |
    • 301 |
    • 302 |
      path: any
      303 |
    • 304 |
    • 305 |
      Optional value: any
      306 |
    • 307 |
    308 |

    Returns any

    309 |
  • 310 |
311 |
312 |
313 |
314 | 360 |
361 |
362 |
363 |
364 |

Legend

365 |
366 |
    367 |
  • Static method
  • 368 |
369 |
370 |
371 |
372 |
373 |

Generated using TypeDoc

374 |
375 |
376 | 377 | 378 | 379 | -------------------------------------------------------------------------------- /docs/globals.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | @astronautlabs/jsonpath 7 | 8 | 9 | 10 | 11 | 12 |
13 |
14 |
15 |
16 | 27 |
28 |
29 | Options 30 |
31 |
32 | All 33 |
    34 |
  • Public
  • 35 |
  • Public/Protected
  • 36 |
  • All
  • 37 |
38 |
39 | 40 | 41 |
42 |
43 | Menu 44 |
45 |
46 |
47 |
48 |
49 |
50 | 55 |

@astronautlabs/jsonpath

56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |

Index

64 |
65 |
66 |
67 |

Classes

68 | 71 |
72 |
73 |
74 |
75 |
76 | 92 |
93 |
94 |
95 |
96 |

Legend

97 |
98 |
    99 |
  • Static method
  • 100 |
101 |
102 |
103 |
104 |
105 |

Generated using TypeDoc

106 |
107 |
108 | 109 | 110 | 111 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | @astronautlabs/jsonpath 7 | 8 | 9 | 10 | 11 | 12 |
13 |
14 |
15 |
16 | 27 |
28 |
29 | Options 30 |
31 |
32 | All 33 |
    34 |
  • Public
  • 35 |
  • Public/Protected
  • 36 |
  • All
  • 37 |
38 |
39 | 40 | 41 |
42 |
43 | Menu 44 |
45 |
46 |
47 |
48 |
49 |
50 | 55 |

@astronautlabs/jsonpath

56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 | 64 |

@astronautlabs/jsonpath CircleCI

65 | 66 |

npm (scoped)

67 |

Query JavaScript objects with JSONPath expressions. 68 | Robust / safe JSONPath engine for Node.js.

69 | 70 |

Query Example

71 |
72 |
import { JSONPath } from '@astronautlabs/jsonpath';
 73 | 
 74 | let cities = [
 75 |   { name: "London", "population": 8615246 },
 76 |   { name: "Berlin", "population": 3517424 },
 77 |   { name: "Madrid", "population": 3165235 },
 78 |   { name: "Rome",   "population": 2870528 }
 79 | ];
 80 | 
 81 | let names = JSONPath.query(cities, '$..name');
 82 | 
 83 | // [ "London", "Berlin", "Madrid", "Rome" ]
84 | 85 |

Install

86 |
87 |

Install from npm:

88 |
$ npm install @astronautlabs/jsonpath
89 | 90 |

JSONPath Syntax

91 |
92 |

Here are syntax and examples adapted from Stefan Goessner's original post introducing JSONPath in 2007.

93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 |
JSONPathDescription
$The root object/element
@The current object/element
.Child member operator
..Recursive descendant operator; JSONPath borrows this syntax from E4X
*Wildcard matching all objects/elements regardless their names
[]Subscript operator
[,]Union operator for alternate names or array indices as a set
[start:end:step]Array slice operator borrowed from ES4 / Python
?()Applies a filter (script) expression via static evaluation
()Script expression via static evaluation
141 |

Given this sample data set, see example expressions below:

142 |
{
143 |   "store": {
144 |     "book": [ 
145 |       {
146 |         "category": "reference",
147 |         "author": "Nigel Rees",
148 |         "title": "Sayings of the Century",
149 |         "price": 8.95
150 |       }, {
151 |         "category": "fiction",
152 |         "author": "Evelyn Waugh",
153 |         "title": "Sword of Honour",
154 |         "price": 12.99
155 |       }, {
156 |         "category": "fiction",
157 |         "author": "Herman Melville",
158 |         "title": "Moby Dick",
159 |         "isbn": "0-553-21311-3",
160 |         "price": 8.99
161 |       }, {
162 |          "category": "fiction",
163 |         "author": "J. R. R. Tolkien",
164 |         "title": "The Lord of the Rings",
165 |         "isbn": "0-395-19395-8",
166 |         "price": 22.99
167 |       }
168 |     ],
169 |     "bicycle": {
170 |       "color": "red",
171 |       "price": 19.95
172 |     }
173 |   }
174 | }
175 |

Example JSONPath expressions:

176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 |
JSONPathDescription
$.store.book[*].authorThe authors of all books in the store
$..authorAll authors
$.store.*All things in store, which are some books and a red bicycle
$.store..priceThe price of everything in the store
$..book[2]The third book
$..book[(@.length-1)]The last book via script subscript
$..book[-1:]The last book via slice
$..book[0,1]The first two books via subscript union
$..book[:2]The first two books via subscript array slice
$..book[?(@.isbn)]Filter all books with isbn number
$..book[?(@.price<10)]Filter all books cheaper than 10
$..book[?(@.price==8.95)]Filter all books that cost 8.95
$..book[?(@.price<30 && @.category=="fiction")]Filter all fiction books cheaper than 30
$..*All members of JSON structure
240 | 241 |

Methods

242 |
243 | 244 |

JSONPath.query(obj, pathExpression[, count])

245 |
246 |

Find elements in obj matching pathExpression. Returns an array of elements that satisfy the provided JSONPath expression, or an empty array if none were matched. Returns only first count elements if specified.

247 |
let authors = jp.query(data, '$..author');
248 | // [ 'Nigel Rees', 'Evelyn Waugh', 'Herman Melville', 'J. R. R. Tolkien' ]
249 | 250 |

JSONPath.paths(obj, pathExpression[, count])

251 |
252 |

Find paths to elements in obj matching pathExpression. Returns an array of element paths that satisfy the provided JSONPath expression. Each path is itself an array of keys representing the location within obj of the matching element. Returns only first count paths if specified.

253 |
let paths = jp.paths(data, '$..author');
254 | // [
255 | //   ['$', 'store', 'book', 0, 'author'] },
256 | //   ['$', 'store', 'book', 1, 'author'] },
257 | //   ['$', 'store', 'book', 2, 'author'] },
258 | //   ['$', 'store', 'book', 3, 'author'] }
259 | // ]
260 | 261 |

JSONPath.nodes(obj, pathExpression[, count])

262 |
263 |

Find elements and their corresponding paths in obj matching pathExpression. Returns an array of node objects where each node has a path containing an array of keys representing the location within obj, and a value pointing to the matched element. Returns only first count nodes if specified.

264 |
let nodes = jp.nodes(data, '$..author');
265 | // [
266 | //   { path: ['$', 'store', 'book', 0, 'author'], value: 'Nigel Rees' },
267 | //   { path: ['$', 'store', 'book', 1, 'author'], value: 'Evelyn Waugh' },
268 | //   { path: ['$', 'store', 'book', 2, 'author'], value: 'Herman Melville' },
269 | //   { path: ['$', 'store', 'book', 3, 'author'], value: 'J. R. R. Tolkien' }
270 | // ]
271 | 272 |

JSONPath.value(obj, pathExpression[, newValue])

273 |
274 |

Returns the value of the first element matching pathExpression. If newValue is provided, sets the value of the first matching element and returns the new value.

275 | 276 |

JSONPath.parent(obj, pathExpression)

277 |
278 |

Returns the parent of the first matching element.

279 | 280 |

JSONPath.apply(obj, pathExpression, fn)

281 |
282 |

Runs the supplied function fn on each matching element, and replaces each matching element with the return value from the function. The function accepts the value of the matching element as its only parameter. Returns matching nodes with their updated values.

283 |
let nodes = jp.apply(data, '$..author', function(value) { return value.toUpperCase() });
284 | // [
285 | //   { path: ['$', 'store', 'book', 0, 'author'], value: 'NIGEL REES' },
286 | //   { path: ['$', 'store', 'book', 1, 'author'], value: 'EVELYN WAUGH' },
287 | //   { path: ['$', 'store', 'book', 2, 'author'], value: 'HERMAN MELVILLE' },
288 | //   { path: ['$', 'store', 'book', 3, 'author'], value: 'J. R. R. TOLKIEN' }
289 | // ]
290 | 291 |

JSONPath.parse(pathExpression)

292 |
293 |

Parse the provided JSONPath expression into path components and their associated operations.

294 |
let path = jp.parse('$..author');
295 | // [
296 | //   { expression: { type: 'root', value: '$' } },
297 | //   { expression: { type: 'identifier', value: 'author' }, operation: 'member', scope: 'descendant' }
298 | // ]
299 | 300 |

JSONPath.stringify(path)

301 |
302 |

Returns a path expression in string form, given a path. The supplied path may either be a flat array of keys, as returned by jp.nodes for example, or may alternatively be a fully parsed path expression in the form of an array of path components as returned by jp.parse.

303 |
let pathExpression = jp.stringify(['$', 'store', 'book', 0, 'author']);
304 | // "$.store.book[0].author"
305 | 306 |

Differences from Original Implementation

307 |
308 |

This implementation aims to be compatible with Stefan Goessner's original implementation with a few notable exceptions described below.

309 | 310 |

Evaluating Script Expressions

311 |
312 |

Script expressions (i.e, (...) and ?(...)) are statically evaluated via static-eval rather than using the underlying script engine directly. That means both that the scope is limited to the instance variable (@), and only simple expressions (with no side effects) will be valid. So for example, ?(@.length>10) will be just fine to match arrays with more than ten elements, but ?(process.exit()) will not get evaluated since process would yield a ReferenceError. This method is even safer than vm.runInNewContext, since the script engine itself is more limited and entirely distinct from the one running the application code. See more details in the implementation of the evaluator.

313 | 314 |

Grammar

315 |
316 |

This project uses a formal BNF grammar to parse JSONPath expressions, an attempt at reverse-engineering the intent of the original implementation, which parses via a series of creative regular expressions. The original regex approach can sometimes be forgiving for better or for worse (e.g., $['store] => $['store']), and in other cases, can be just plain wrong (e.g. [ => $).

317 | 318 |

Other Minor Differences

319 |
320 |

As a result of using a real parser and static evaluation, there are some arguable bugs in the original library that have not been carried through here:

321 |
    322 |
  • strings in subscripts may now be double-quoted
  • 323 |
  • final step arguments in slice operators may now be negative
  • 324 |
  • script expressions may now contain . and @ characters not referring to instance variables
  • 325 |
  • subscripts no longer act as character slices on string elements
  • 326 |
  • non-ascii non-word characters are no-longer valid in member identifier names; use quoted subscript strings instead (e.g., $['$'] instead of $.$)
  • 327 |
  • unions now yield real unions with no duplicates rather than concatenated results
  • 328 |
329 | 330 |

License

331 |
332 |

MIT

333 |
334 |
335 | 351 |
352 |
353 |
354 |
355 |

Legend

356 |
357 |
    358 |
  • Static method
  • 359 |
360 |
361 |
362 |
363 |
364 |

Generated using TypeDoc

365 |
366 |
367 | 368 | 369 | 370 | -------------------------------------------------------------------------------- /fixtures/store.json: -------------------------------------------------------------------------------- 1 | { "store": { 2 | "book": [ 3 | { "category": "reference", 4 | "author": "Nigel Rees", 5 | "title": "Sayings of the Century", 6 | "price": 8.95 7 | }, 8 | { "category": "fiction", 9 | "author": "Evelyn Waugh", 10 | "title": "Sword of Honour", 11 | "price": 12.99 12 | }, 13 | { "category": "fiction", 14 | "author": "Herman Melville", 15 | "title": "Moby Dick", 16 | "isbn": "0-553-21311-3", 17 | "price": 8.99 18 | }, 19 | { "category": "fiction", 20 | "author": "J. R. R. Tolkien", 21 | "title": "The Lord of the Rings", 22 | "isbn": "0-395-19395-8", 23 | "price": 22.99 24 | } 25 | ], 26 | "bicycle": { 27 | "color": "red", 28 | "price": 19.95 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /generate-parser.ts: -------------------------------------------------------------------------------- 1 | import { Jison } from 'jison'; 2 | import * as fs from 'fs'; 3 | import * as path from 'path'; 4 | import * as mkdirp from 'mkdirp'; 5 | import { GRAMMAR } from './src/grammar'; 6 | 7 | let parser = Jison.Parser(GRAMMAR); 8 | let source = parser.generate(); 9 | let generatedDir = path.join(__dirname, 'generated'); 10 | 11 | source = source.replace(/exports\.main.*/ms, `\n}`); 12 | 13 | mkdirp.sync(generatedDir); 14 | fs.writeFileSync(path.join(generatedDir, 'parser.js'), source); -------------------------------------------------------------------------------- /generated/parser.js: -------------------------------------------------------------------------------- 1 | /* parser generated by jison 0.4.18 */ 2 | /* 3 | Returns a Parser object of the following structure: 4 | 5 | Parser: { 6 | yy: {} 7 | } 8 | 9 | Parser.prototype: { 10 | yy: {}, 11 | trace: function(), 12 | symbols_: {associative list: name ==> number}, 13 | terminals_: {associative list: number ==> name}, 14 | productions_: [...], 15 | performAction: function anonymous(yytext, yyleng, yylineno, yy, yystate, $$, _$), 16 | table: [...], 17 | defaultActions: {...}, 18 | parseError: function(str, hash), 19 | parse: function(input), 20 | 21 | lexer: { 22 | EOF: 1, 23 | parseError: function(str, hash), 24 | setInput: function(input), 25 | input: function(), 26 | unput: function(str), 27 | more: function(), 28 | less: function(n), 29 | pastInput: function(), 30 | upcomingInput: function(), 31 | showPosition: function(), 32 | test_match: function(regex_match_array, rule_index), 33 | next: function(), 34 | lex: function(), 35 | begin: function(condition), 36 | popState: function(), 37 | _currentRules: function(), 38 | topState: function(), 39 | pushState: function(condition), 40 | 41 | options: { 42 | ranges: boolean (optional: true ==> token location info will include a .range[] member) 43 | flex: boolean (optional: true ==> flex-like lexing behaviour where the rules are tested exhaustively to find the longest match) 44 | backtrack_lexer: boolean (optional: true ==> lexer regexes are tested in order and for each matching regex the action code is invoked; the lexer terminates the scan when a token is returned by the action code) 45 | }, 46 | 47 | performAction: function(yy, yy_, $avoiding_name_collisions, YY_START), 48 | rules: [...], 49 | conditions: {associative list: name ==> set}, 50 | } 51 | } 52 | 53 | 54 | token location info (@$, _$, etc.): { 55 | first_line: n, 56 | last_line: n, 57 | first_column: n, 58 | last_column: n, 59 | range: [start_number, end_number] (where the numbers are indexes into the input string, regular zero-based) 60 | } 61 | 62 | 63 | the parseError function receives a 'hash' object with these members for lexer and parser errors: { 64 | text: (matched text) 65 | token: (the produced terminal token, if any) 66 | line: (yylineno) 67 | } 68 | while parser (grammar) errors will also provide these members, i.e. parser errors deliver a superset of attributes: { 69 | loc: (yylloc) 70 | expected: (string describing the set of expected tokens) 71 | recoverable: (boolean: TRUE when the parser has a error recovery rule available for this particular error) 72 | } 73 | */ 74 | var parser = (function(){ 75 | var o=function(k,v,o,l){for(o=o||{},l=k.length;l--;o[k[l]]=v);return o},$V0=[1,5],$V1=[1,6],$V2=[1,7],$V3=[1,8],$V4=[1,9],$V5=[1,18],$V6=[1,19],$V7=[1,20],$V8=[1,12,14,22],$V9=[1,29],$Va=[1,30],$Vb=[1,33],$Vc=[1,35],$Vd=[1,31],$Ve=[1,36],$Vf=[1,37],$Vg=[24,28]; 76 | var parser = {trace: function trace () { }, 77 | yy: {}, 78 | symbols_: {"error":2,"JSON_PATH":3,"DOLLAR":4,"PATH_COMPONENTS":5,"LEADING_CHILD_MEMBER_EXPRESSION":6,"PATH_COMPONENT":7,"MEMBER_COMPONENT":8,"SUBSCRIPT_COMPONENT":9,"CHILD_MEMBER_COMPONENT":10,"DESCENDANT_MEMBER_COMPONENT":11,"DOT":12,"MEMBER_EXPRESSION":13,"DOT_DOT":14,"STAR":15,"IDENTIFIER":16,"SCRIPT_EXPRESSION":17,"INTEGER":18,"END":19,"CHILD_SUBSCRIPT_COMPONENT":20,"DESCENDANT_SUBSCRIPT_COMPONENT":21,"[":22,"SUBSCRIPT":23,"]":24,"SUBSCRIPT_EXPRESSION":25,"SUBSCRIPT_EXPRESSION_LIST":26,"SUBSCRIPT_EXPRESSION_LISTABLE":27,",":28,"STRING_LITERAL":29,"ARRAY_SLICE":30,"FILTER_EXPRESSION":31,"QQ_STRING":32,"Q_STRING":33,"$accept":0,"$end":1}, 79 | terminals_: {2:"error",4:"DOLLAR",12:"DOT",14:"DOT_DOT",15:"STAR",16:"IDENTIFIER",17:"SCRIPT_EXPRESSION",18:"INTEGER",19:"END",22:"[",24:"]",28:",",30:"ARRAY_SLICE",31:"FILTER_EXPRESSION",32:"QQ_STRING",33:"Q_STRING"}, 80 | productions_: [0,[3,1],[3,2],[3,1],[3,2],[5,1],[5,2],[7,1],[7,1],[8,1],[8,1],[10,2],[6,1],[11,2],[13,1],[13,1],[13,1],[13,1],[13,1],[9,1],[9,1],[20,3],[21,4],[23,1],[23,1],[26,1],[26,3],[27,1],[27,1],[27,1],[25,1],[25,1],[25,1],[29,1],[29,1]], 81 | performAction: function anonymous(yytext, yyleng, yylineno, yy, yystate /* action[1] */, $$ /* vstack */, _$ /* lstack */) { 82 | /* this == yyval */ 83 | if (!yy.ast) { 84 | yy.ast = _ast; 85 | _ast.initialize(); 86 | } 87 | 88 | var $0 = $$.length - 1; 89 | switch (yystate) { 90 | case 1: 91 | yy.ast.set({ expression: { type: "root", value: $$[$0] } }); yy.ast.unshift(); return yy.ast.yield() 92 | break; 93 | case 2: 94 | yy.ast.set({ expression: { type: "root", value: $$[$0-1] } }); yy.ast.unshift(); return yy.ast.yield() 95 | break; 96 | case 3: 97 | yy.ast.unshift(); return yy.ast.yield() 98 | break; 99 | case 4: 100 | yy.ast.set({ operation: "member", scope: "child", expression: { type: "identifier", value: $$[$0-1] }}); yy.ast.unshift(); return yy.ast.yield() 101 | break; 102 | case 5: case 6: case 11: case 13: case 18: case 21: case 22: case 23: 103 | 104 | break; 105 | case 7: 106 | yy.ast.set({ operation: "member" }); yy.ast.push() 107 | break; 108 | case 8: 109 | yy.ast.set({ operation: "subscript" }); yy.ast.push() 110 | break; 111 | case 9: case 19: 112 | yy.ast.set({ scope: "child" }) 113 | break; 114 | case 10: case 20: 115 | yy.ast.set({ scope: "descendant" }) 116 | break; 117 | case 12: 118 | yy.ast.set({ scope: "child", operation: "member" }) 119 | break; 120 | case 14: 121 | yy.ast.set({ expression: { type: "wildcard", value: $$[$0] } }) 122 | break; 123 | case 15: 124 | yy.ast.set({ expression: { type: "identifier", value: $$[$0] } }) 125 | break; 126 | case 16: 127 | yy.ast.set({ expression: { type: "script_expression", value: $$[$0] } }) 128 | break; 129 | case 17: 130 | yy.ast.set({ expression: { type: "numeric_literal", value: parseInt($$[$0]) } }) 131 | break; 132 | case 24: 133 | $$[$0].length > 1? yy.ast.set({ expression: { type: "union", value: $$[$0] } }) : this.$ = $$[$0] 134 | break; 135 | case 25: 136 | this.$ = [$$[$0]] 137 | break; 138 | case 26: 139 | this.$ = $$[$0-2].concat($$[$0]) 140 | break; 141 | case 27: 142 | this.$ = { expression: { type: "numeric_literal", value: parseInt($$[$0]) } }; yy.ast.set(this.$) 143 | break; 144 | case 28: 145 | this.$ = { expression: { type: "string_literal", value: $$[$0] } }; yy.ast.set(this.$) 146 | break; 147 | case 29: 148 | this.$ = { expression: { type: "slice", value: $$[$0] } }; yy.ast.set(this.$) 149 | break; 150 | case 30: 151 | this.$ = { expression: { type: "wildcard", value: $$[$0] } }; yy.ast.set(this.$) 152 | break; 153 | case 31: 154 | this.$ = { expression: { type: "script_expression", value: $$[$0] } }; yy.ast.set(this.$) 155 | break; 156 | case 32: 157 | this.$ = { expression: { type: "filter_expression", value: $$[$0] } }; yy.ast.set(this.$) 158 | break; 159 | case 33: case 34: 160 | this.$ = $$[$0] 161 | break; 162 | } 163 | }, 164 | table: [{3:1,4:[1,2],6:3,13:4,15:$V0,16:$V1,17:$V2,18:$V3,19:$V4},{1:[3]},{1:[2,1],5:10,7:11,8:12,9:13,10:14,11:15,12:$V5,14:$V6,20:16,21:17,22:$V7},{1:[2,3],5:21,7:11,8:12,9:13,10:14,11:15,12:$V5,14:$V6,20:16,21:17,22:$V7},o($V8,[2,12]),o($V8,[2,14]),o($V8,[2,15]),o($V8,[2,16]),o($V8,[2,17]),o($V8,[2,18]),{1:[2,2],7:22,8:12,9:13,10:14,11:15,12:$V5,14:$V6,20:16,21:17,22:$V7},o($V8,[2,5]),o($V8,[2,7]),o($V8,[2,8]),o($V8,[2,9]),o($V8,[2,10]),o($V8,[2,19]),o($V8,[2,20]),{13:23,15:$V0,16:$V1,17:$V2,18:$V3,19:$V4},{13:24,15:$V0,16:$V1,17:$V2,18:$V3,19:$V4,22:[1,25]},{15:$V9,17:$Va,18:$Vb,23:26,25:27,26:28,27:32,29:34,30:$Vc,31:$Vd,32:$Ve,33:$Vf},{1:[2,4],7:22,8:12,9:13,10:14,11:15,12:$V5,14:$V6,20:16,21:17,22:$V7},o($V8,[2,6]),o($V8,[2,11]),o($V8,[2,13]),{15:$V9,17:$Va,18:$Vb,23:38,25:27,26:28,27:32,29:34,30:$Vc,31:$Vd,32:$Ve,33:$Vf},{24:[1,39]},{24:[2,23]},{24:[2,24],28:[1,40]},{24:[2,30]},{24:[2,31]},{24:[2,32]},o($Vg,[2,25]),o($Vg,[2,27]),o($Vg,[2,28]),o($Vg,[2,29]),o($Vg,[2,33]),o($Vg,[2,34]),{24:[1,41]},o($V8,[2,21]),{18:$Vb,27:42,29:34,30:$Vc,32:$Ve,33:$Vf},o($V8,[2,22]),o($Vg,[2,26])], 165 | defaultActions: {27:[2,23],29:[2,30],30:[2,31],31:[2,32]}, 166 | parseError: function parseError (str, hash) { 167 | if (hash.recoverable) { 168 | this.trace(str); 169 | } else { 170 | var error = new Error(str); 171 | error.hash = hash; 172 | throw error; 173 | } 174 | }, 175 | parse: function parse(input) { 176 | var self = this, stack = [0], tstack = [], vstack = [null], lstack = [], table = this.table, yytext = '', yylineno = 0, yyleng = 0, recovering = 0, TERROR = 2, EOF = 1; 177 | var args = lstack.slice.call(arguments, 1); 178 | var lexer = Object.create(this.lexer); 179 | var sharedState = { yy: {} }; 180 | for (var k in this.yy) { 181 | if (Object.prototype.hasOwnProperty.call(this.yy, k)) { 182 | sharedState.yy[k] = this.yy[k]; 183 | } 184 | } 185 | lexer.setInput(input, sharedState.yy); 186 | sharedState.yy.lexer = lexer; 187 | sharedState.yy.parser = this; 188 | if (typeof lexer.yylloc == 'undefined') { 189 | lexer.yylloc = {}; 190 | } 191 | var yyloc = lexer.yylloc; 192 | lstack.push(yyloc); 193 | var ranges = lexer.options && lexer.options.ranges; 194 | if (typeof sharedState.yy.parseError === 'function') { 195 | this.parseError = sharedState.yy.parseError; 196 | } else { 197 | this.parseError = Object.getPrototypeOf(this).parseError; 198 | } 199 | function popStack(n) { 200 | stack.length = stack.length - 2 * n; 201 | vstack.length = vstack.length - n; 202 | lstack.length = lstack.length - n; 203 | } 204 | _token_stack: 205 | var lex = function () { 206 | var token; 207 | token = lexer.lex() || EOF; 208 | if (typeof token !== 'number') { 209 | token = self.symbols_[token] || token; 210 | } 211 | return token; 212 | }; 213 | var symbol, preErrorSymbol, state, action, a, r, yyval = {}, p, len, newState, expected; 214 | while (true) { 215 | state = stack[stack.length - 1]; 216 | if (this.defaultActions[state]) { 217 | action = this.defaultActions[state]; 218 | } else { 219 | if (symbol === null || typeof symbol == 'undefined') { 220 | symbol = lex(); 221 | } 222 | action = table[state] && table[state][symbol]; 223 | } 224 | if (typeof action === 'undefined' || !action.length || !action[0]) { 225 | var errStr = ''; 226 | expected = []; 227 | for (p in table[state]) { 228 | if (this.terminals_[p] && p > TERROR) { 229 | expected.push('\'' + this.terminals_[p] + '\''); 230 | } 231 | } 232 | if (lexer.showPosition) { 233 | errStr = 'Parse error on line ' + (yylineno + 1) + ':\n' + lexer.showPosition() + '\nExpecting ' + expected.join(', ') + ', got \'' + (this.terminals_[symbol] || symbol) + '\''; 234 | } else { 235 | errStr = 'Parse error on line ' + (yylineno + 1) + ': Unexpected ' + (symbol == EOF ? 'end of input' : '\'' + (this.terminals_[symbol] || symbol) + '\''); 236 | } 237 | this.parseError(errStr, { 238 | text: lexer.match, 239 | token: this.terminals_[symbol] || symbol, 240 | line: lexer.yylineno, 241 | loc: yyloc, 242 | expected: expected 243 | }); 244 | } 245 | if (action[0] instanceof Array && action.length > 1) { 246 | throw new Error('Parse Error: multiple actions possible at state: ' + state + ', token: ' + symbol); 247 | } 248 | switch (action[0]) { 249 | case 1: 250 | stack.push(symbol); 251 | vstack.push(lexer.yytext); 252 | lstack.push(lexer.yylloc); 253 | stack.push(action[1]); 254 | symbol = null; 255 | if (!preErrorSymbol) { 256 | yyleng = lexer.yyleng; 257 | yytext = lexer.yytext; 258 | yylineno = lexer.yylineno; 259 | yyloc = lexer.yylloc; 260 | if (recovering > 0) { 261 | recovering--; 262 | } 263 | } else { 264 | symbol = preErrorSymbol; 265 | preErrorSymbol = null; 266 | } 267 | break; 268 | case 2: 269 | len = this.productions_[action[1]][1]; 270 | yyval.$ = vstack[vstack.length - len]; 271 | yyval._$ = { 272 | first_line: lstack[lstack.length - (len || 1)].first_line, 273 | last_line: lstack[lstack.length - 1].last_line, 274 | first_column: lstack[lstack.length - (len || 1)].first_column, 275 | last_column: lstack[lstack.length - 1].last_column 276 | }; 277 | if (ranges) { 278 | yyval._$.range = [ 279 | lstack[lstack.length - (len || 1)].range[0], 280 | lstack[lstack.length - 1].range[1] 281 | ]; 282 | } 283 | r = this.performAction.apply(yyval, [ 284 | yytext, 285 | yyleng, 286 | yylineno, 287 | sharedState.yy, 288 | action[1], 289 | vstack, 290 | lstack 291 | ].concat(args)); 292 | if (typeof r !== 'undefined') { 293 | return r; 294 | } 295 | if (len) { 296 | stack = stack.slice(0, -1 * len * 2); 297 | vstack = vstack.slice(0, -1 * len); 298 | lstack = lstack.slice(0, -1 * len); 299 | } 300 | stack.push(this.productions_[action[1]][0]); 301 | vstack.push(yyval.$); 302 | lstack.push(yyval._$); 303 | newState = table[stack[stack.length - 2]][stack[stack.length - 1]]; 304 | stack.push(newState); 305 | break; 306 | case 3: 307 | return true; 308 | } 309 | } 310 | return true; 311 | }}; 312 | var _ast = { 313 | 314 | initialize: function() { 315 | this._nodes = []; 316 | this._node = {}; 317 | this._stash = []; 318 | }, 319 | 320 | set: function(props) { 321 | for (var k in props) this._node[k] = props[k]; 322 | return this._node; 323 | }, 324 | 325 | node: function(obj) { 326 | if (arguments.length) this._node = obj; 327 | return this._node; 328 | }, 329 | 330 | push: function() { 331 | this._nodes.push(this._node); 332 | this._node = {}; 333 | }, 334 | 335 | unshift: function() { 336 | this._nodes.unshift(this._node); 337 | this._node = {}; 338 | }, 339 | 340 | yield: function() { 341 | var _nodes = this._nodes; 342 | this.initialize(); 343 | return _nodes; 344 | } 345 | }; 346 | /* generated by jison-lex 0.3.4 */ 347 | var lexer = (function(){ 348 | var lexer = ({ 349 | 350 | EOF:1, 351 | 352 | parseError:function parseError(str, hash) { 353 | if (this.yy.parser) { 354 | this.yy.parser.parseError(str, hash); 355 | } else { 356 | throw new Error(str); 357 | } 358 | }, 359 | 360 | // resets the lexer, sets new input 361 | setInput:function (input, yy) { 362 | this.yy = yy || this.yy || {}; 363 | this._input = input; 364 | this._more = this._backtrack = this.done = false; 365 | this.yylineno = this.yyleng = 0; 366 | this.yytext = this.matched = this.match = ''; 367 | this.conditionStack = ['INITIAL']; 368 | this.yylloc = { 369 | first_line: 1, 370 | first_column: 0, 371 | last_line: 1, 372 | last_column: 0 373 | }; 374 | if (this.options.ranges) { 375 | this.yylloc.range = [0,0]; 376 | } 377 | this.offset = 0; 378 | return this; 379 | }, 380 | 381 | // consumes and returns one char from the input 382 | input:function () { 383 | var ch = this._input[0]; 384 | this.yytext += ch; 385 | this.yyleng++; 386 | this.offset++; 387 | this.match += ch; 388 | this.matched += ch; 389 | var lines = ch.match(/(?:\r\n?|\n).*/g); 390 | if (lines) { 391 | this.yylineno++; 392 | this.yylloc.last_line++; 393 | } else { 394 | this.yylloc.last_column++; 395 | } 396 | if (this.options.ranges) { 397 | this.yylloc.range[1]++; 398 | } 399 | 400 | this._input = this._input.slice(1); 401 | return ch; 402 | }, 403 | 404 | // unshifts one char (or a string) into the input 405 | unput:function (ch) { 406 | var len = ch.length; 407 | var lines = ch.split(/(?:\r\n?|\n)/g); 408 | 409 | this._input = ch + this._input; 410 | this.yytext = this.yytext.substr(0, this.yytext.length - len); 411 | //this.yyleng -= len; 412 | this.offset -= len; 413 | var oldLines = this.match.split(/(?:\r\n?|\n)/g); 414 | this.match = this.match.substr(0, this.match.length - 1); 415 | this.matched = this.matched.substr(0, this.matched.length - 1); 416 | 417 | if (lines.length - 1) { 418 | this.yylineno -= lines.length - 1; 419 | } 420 | var r = this.yylloc.range; 421 | 422 | this.yylloc = { 423 | first_line: this.yylloc.first_line, 424 | last_line: this.yylineno + 1, 425 | first_column: this.yylloc.first_column, 426 | last_column: lines ? 427 | (lines.length === oldLines.length ? this.yylloc.first_column : 0) 428 | + oldLines[oldLines.length - lines.length].length - lines[0].length : 429 | this.yylloc.first_column - len 430 | }; 431 | 432 | if (this.options.ranges) { 433 | this.yylloc.range = [r[0], r[0] + this.yyleng - len]; 434 | } 435 | this.yyleng = this.yytext.length; 436 | return this; 437 | }, 438 | 439 | // When called from action, caches matched text and appends it on next action 440 | more:function () { 441 | this._more = true; 442 | return this; 443 | }, 444 | 445 | // When called from action, signals the lexer that this rule fails to match the input, so the next matching rule (regex) should be tested instead. 446 | reject:function () { 447 | if (this.options.backtrack_lexer) { 448 | this._backtrack = true; 449 | } else { 450 | return this.parseError('Lexical error on line ' + (this.yylineno + 1) + '. You can only invoke reject() in the lexer when the lexer is of the backtracking persuasion (options.backtrack_lexer = true).\n' + this.showPosition(), { 451 | text: "", 452 | token: null, 453 | line: this.yylineno 454 | }); 455 | 456 | } 457 | return this; 458 | }, 459 | 460 | // retain first n characters of the match 461 | less:function (n) { 462 | this.unput(this.match.slice(n)); 463 | }, 464 | 465 | // displays already matched input, i.e. for error messages 466 | pastInput:function () { 467 | var past = this.matched.substr(0, this.matched.length - this.match.length); 468 | return (past.length > 20 ? '...':'') + past.substr(-20).replace(/\n/g, ""); 469 | }, 470 | 471 | // displays upcoming input, i.e. for error messages 472 | upcomingInput:function () { 473 | var next = this.match; 474 | if (next.length < 20) { 475 | next += this._input.substr(0, 20-next.length); 476 | } 477 | return (next.substr(0,20) + (next.length > 20 ? '...' : '')).replace(/\n/g, ""); 478 | }, 479 | 480 | // displays the character position where the lexing error occurred, i.e. for error messages 481 | showPosition:function () { 482 | var pre = this.pastInput(); 483 | var c = new Array(pre.length + 1).join("-"); 484 | return pre + this.upcomingInput() + "\n" + c + "^"; 485 | }, 486 | 487 | // test the lexed token: return FALSE when not a match, otherwise return token 488 | test_match:function(match, indexed_rule) { 489 | var token, 490 | lines, 491 | backup; 492 | 493 | if (this.options.backtrack_lexer) { 494 | // save context 495 | backup = { 496 | yylineno: this.yylineno, 497 | yylloc: { 498 | first_line: this.yylloc.first_line, 499 | last_line: this.last_line, 500 | first_column: this.yylloc.first_column, 501 | last_column: this.yylloc.last_column 502 | }, 503 | yytext: this.yytext, 504 | match: this.match, 505 | matches: this.matches, 506 | matched: this.matched, 507 | yyleng: this.yyleng, 508 | offset: this.offset, 509 | _more: this._more, 510 | _input: this._input, 511 | yy: this.yy, 512 | conditionStack: this.conditionStack.slice(0), 513 | done: this.done 514 | }; 515 | if (this.options.ranges) { 516 | backup.yylloc.range = this.yylloc.range.slice(0); 517 | } 518 | } 519 | 520 | lines = match[0].match(/(?:\r\n?|\n).*/g); 521 | if (lines) { 522 | this.yylineno += lines.length; 523 | } 524 | this.yylloc = { 525 | first_line: this.yylloc.last_line, 526 | last_line: this.yylineno + 1, 527 | first_column: this.yylloc.last_column, 528 | last_column: lines ? 529 | lines[lines.length - 1].length - lines[lines.length - 1].match(/\r?\n?/)[0].length : 530 | this.yylloc.last_column + match[0].length 531 | }; 532 | this.yytext += match[0]; 533 | this.match += match[0]; 534 | this.matches = match; 535 | this.yyleng = this.yytext.length; 536 | if (this.options.ranges) { 537 | this.yylloc.range = [this.offset, this.offset += this.yyleng]; 538 | } 539 | this._more = false; 540 | this._backtrack = false; 541 | this._input = this._input.slice(match[0].length); 542 | this.matched += match[0]; 543 | token = this.performAction.call(this, this.yy, this, indexed_rule, this.conditionStack[this.conditionStack.length - 1]); 544 | if (this.done && this._input) { 545 | this.done = false; 546 | } 547 | if (token) { 548 | return token; 549 | } else if (this._backtrack) { 550 | // recover context 551 | for (var k in backup) { 552 | this[k] = backup[k]; 553 | } 554 | return false; // rule action called reject() implying the next rule should be tested instead. 555 | } 556 | return false; 557 | }, 558 | 559 | // return next match in input 560 | next:function () { 561 | if (this.done) { 562 | return this.EOF; 563 | } 564 | if (!this._input) { 565 | this.done = true; 566 | } 567 | 568 | var token, 569 | match, 570 | tempMatch, 571 | index; 572 | if (!this._more) { 573 | this.yytext = ''; 574 | this.match = ''; 575 | } 576 | var rules = this._currentRules(); 577 | for (var i = 0; i < rules.length; i++) { 578 | tempMatch = this._input.match(this.rules[rules[i]]); 579 | if (tempMatch && (!match || tempMatch[0].length > match[0].length)) { 580 | match = tempMatch; 581 | index = i; 582 | if (this.options.backtrack_lexer) { 583 | token = this.test_match(tempMatch, rules[i]); 584 | if (token !== false) { 585 | return token; 586 | } else if (this._backtrack) { 587 | match = false; 588 | continue; // rule action called reject() implying a rule MISmatch. 589 | } else { 590 | // else: this is a lexer rule which consumes input without producing a token (e.g. whitespace) 591 | return false; 592 | } 593 | } else if (!this.options.flex) { 594 | break; 595 | } 596 | } 597 | } 598 | if (match) { 599 | token = this.test_match(match, rules[index]); 600 | if (token !== false) { 601 | return token; 602 | } 603 | // else: this is a lexer rule which consumes input without producing a token (e.g. whitespace) 604 | return false; 605 | } 606 | if (this._input === "") { 607 | return this.EOF; 608 | } else { 609 | return this.parseError('Lexical error on line ' + (this.yylineno + 1) + '. Unrecognized text.\n' + this.showPosition(), { 610 | text: "", 611 | token: null, 612 | line: this.yylineno 613 | }); 614 | } 615 | }, 616 | 617 | // return next match that has a token 618 | lex:function lex () { 619 | var r = this.next(); 620 | if (r) { 621 | return r; 622 | } else { 623 | return this.lex(); 624 | } 625 | }, 626 | 627 | // activates a new lexer condition state (pushes the new lexer condition state onto the condition stack) 628 | begin:function begin (condition) { 629 | this.conditionStack.push(condition); 630 | }, 631 | 632 | // pop the previously active lexer condition state off the condition stack 633 | popState:function popState () { 634 | var n = this.conditionStack.length - 1; 635 | if (n > 0) { 636 | return this.conditionStack.pop(); 637 | } else { 638 | return this.conditionStack[0]; 639 | } 640 | }, 641 | 642 | // produce the lexer rule set which is active for the currently active lexer condition state 643 | _currentRules:function _currentRules () { 644 | if (this.conditionStack.length && this.conditionStack[this.conditionStack.length - 1]) { 645 | return this.conditions[this.conditionStack[this.conditionStack.length - 1]].rules; 646 | } else { 647 | return this.conditions["INITIAL"].rules; 648 | } 649 | }, 650 | 651 | // return the currently active lexer condition state; when an index argument is provided it produces the N-th previous condition state, if available 652 | topState:function topState (n) { 653 | n = this.conditionStack.length - 1 - Math.abs(n || 0); 654 | if (n >= 0) { 655 | return this.conditionStack[n]; 656 | } else { 657 | return "INITIAL"; 658 | } 659 | }, 660 | 661 | // alias for begin(condition) 662 | pushState:function pushState (condition) { 663 | this.begin(condition); 664 | }, 665 | 666 | // return the number of states currently on the stack 667 | stateStackSize:function stateStackSize() { 668 | return this.conditionStack.length; 669 | }, 670 | options: {}, 671 | performAction: function anonymous(yy,yy_,$avoiding_name_collisions,YY_START) { 672 | var YYSTATE=YY_START; 673 | switch($avoiding_name_collisions) { 674 | case 0:return 4 675 | break; 676 | case 1:return 14 677 | break; 678 | case 2:return 12 679 | break; 680 | case 3:return 15 681 | break; 682 | case 4:return 16 683 | break; 684 | case 5:return 22 685 | break; 686 | case 6:return 24 687 | break; 688 | case 7:return 28 689 | break; 690 | case 8:return 30 691 | break; 692 | case 9:return 18 693 | break; 694 | case 10:yy_.yytext = yy_.yytext.substr(1,yy_.yyleng-2); return 32; 695 | break; 696 | case 11:yy_.yytext = yy_.yytext.substr(1,yy_.yyleng-2); return 33; 697 | break; 698 | case 12:return 17 699 | break; 700 | case 13:return 31 701 | break; 702 | } 703 | }, 704 | rules: [/^(?:\$)/,/^(?:\.\.)/,/^(?:\.)/,/^(?:\*)/,/^(?:[a-zA-Z_]+[a-zA-Z0-9_]*)/,/^(?:\[)/,/^(?:\])/,/^(?:,)/,/^(?:((-?(?:0|[1-9][0-9]*)))?\:((-?(?:0|[1-9][0-9]*)))?(\:((-?(?:0|[1-9][0-9]*)))?)?)/,/^(?:(-?(?:0|[1-9][0-9]*)))/,/^(?:"(?:\\["bfnrt\/\\]|\\u[a-fA-F0-9]{4}|[^"\\])*")/,/^(?:'(?:\\['bfnrt\/\\]|\\u[a-fA-F0-9]{4}|[^'\\])*')/,/^(?:\(.+?\)(?=\]))/,/^(?:\?\(.+?\)(?=\]))/], 705 | conditions: {"INITIAL":{"rules":[0,1,2,3,4,5,6,7,8,9,10,11,12,13],"inclusive":true}} 706 | }); 707 | return lexer; 708 | })(); 709 | parser.lexer = lexer; 710 | function Parser () { 711 | this.yy = {}; 712 | } 713 | Parser.prototype = parser;parser.Parser = Parser; 714 | return new Parser; 715 | })(); 716 | 717 | 718 | if (typeof require !== 'undefined' && typeof exports !== 'undefined') { 719 | exports.parser = parser; 720 | exports.Parser = parser.Parser; 721 | exports.parse = function () { return parser.parse.apply(parser, arguments); }; 722 | 723 | } -------------------------------------------------------------------------------- /include/action.js: -------------------------------------------------------------------------------- 1 | if (!yy.ast) { 2 | yy.ast = _ast; 3 | _ast.initialize(); 4 | } 5 | -------------------------------------------------------------------------------- /include/module.js: -------------------------------------------------------------------------------- 1 | var _ast = { 2 | 3 | initialize: function() { 4 | this._nodes = []; 5 | this._node = {}; 6 | this._stash = []; 7 | }, 8 | 9 | set: function(props) { 10 | for (var k in props) this._node[k] = props[k]; 11 | return this._node; 12 | }, 13 | 14 | node: function(obj) { 15 | if (arguments.length) this._node = obj; 16 | return this._node; 17 | }, 18 | 19 | push: function() { 20 | this._nodes.push(this._node); 21 | this._node = {}; 22 | }, 23 | 24 | unshift: function() { 25 | this._nodes.unshift(this._node); 26 | this._node = {}; 27 | }, 28 | 29 | yield: function() { 30 | var _nodes = this._nodes; 31 | this.initialize(); 32 | return _nodes; 33 | } 34 | }; 35 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@astronautlabs/jsonpath", 3 | "private": false, 4 | "description": "Query JavaScript objects with JSONPath expressions. Robust / safe JSONPath engine for Node.js.", 5 | "version": "1.0.8", 6 | "author": { 7 | "name": "Astronaut Labs, LLC.", 8 | "url": "https://astronautlabs.com/" 9 | }, 10 | "main": "./dist/index.js", 11 | "scripts": { 12 | "clean": "rimraf dist", 13 | "build": "npm run clean && tsc -p .", 14 | "prepublishOnly": "npm run build && npm run docs", 15 | "docs": "typedoc src", 16 | "generate": "ts-node generate-parser.ts", 17 | "test": "npm run build && node dist/test.js" 18 | }, 19 | "dependencies": { 20 | "@types/esprima": "^4.0.2", 21 | "@types/mkdirp": "^1.0.0", 22 | "esprima": "1.2.2", 23 | "static-eval": "2.0.2", 24 | "underscore": "1.7.0" 25 | }, 26 | "devDependencies": { 27 | "@types/assert": "^1.4.7", 28 | "@types/chai": "^4.2.11", 29 | "@types/node": "14.0.1", 30 | "@types/static-eval": "^0.2.30", 31 | "@types/underscore": "^1.10.0", 32 | "chai": "^4.2.0", 33 | "grunt": "0.4.5", 34 | "grunt-browserify": "3.8.0", 35 | "grunt-cli": "0.1.13", 36 | "grunt-contrib-uglify": "0.9.1", 37 | "jison": "^0.4.18", 38 | "jscs": "1.10.0", 39 | "jshint": "2.6.0", 40 | "mkdirp": "^1.0.4", 41 | "mocha": "2.1.0", 42 | "razmin": "^0.6.13", 43 | "rimraf": "^3.0.2", 44 | "ts-node": "^8.10.1", 45 | "typedoc": "^0.17.7", 46 | "typescript": "^3.9.3" 47 | }, 48 | "repository": { 49 | "type": "git", 50 | "url": "https://github.com/astronautlabs/jsonpath" 51 | }, 52 | "keywords": [ 53 | "JSONPath", 54 | "jsonpath", 55 | "json-path", 56 | "object", 57 | "traversal", 58 | "json", 59 | "path", 60 | "data structures" 61 | ], 62 | "license": "MIT" 63 | } 64 | -------------------------------------------------------------------------------- /src/assert.ts: -------------------------------------------------------------------------------- 1 | export class assert { 2 | static ok(predicate : boolean, message : string) { 3 | if (!predicate) 4 | throw new Error(message); 5 | } 6 | 7 | static equal(value : any, expected : any, message : string) { 8 | if (value !== expected) 9 | throw new Error(message); 10 | } 11 | } -------------------------------------------------------------------------------- /src/grammar.ts: -------------------------------------------------------------------------------- 1 | import { TOKENS } from './tokens'; 2 | import * as fs from 'fs'; 3 | import * as path from 'path'; 4 | 5 | /** 6 | * @internal 7 | * @hidden 8 | */ 9 | let grammar = { 10 | lex: { 11 | macros: { 12 | esc: "\\\\", 13 | int: TOKENS.integer 14 | }, 15 | 16 | rules: [ 17 | ["\\$", "return 'DOLLAR'"], 18 | ["\\.\\.", "return 'DOT_DOT'"], 19 | ["\\.", "return 'DOT'"], 20 | ["\\*", "return 'STAR'"], 21 | [TOKENS.identifier, "return 'IDENTIFIER'"], 22 | ["\\[", "return '['"], 23 | ["\\]", "return ']'"], 24 | [",", "return ','"], 25 | ["({int})?\\:({int})?(\\:({int})?)?", "return 'ARRAY_SLICE'"], 26 | ["{int}", "return 'INTEGER'"], 27 | [TOKENS.qq_string, "yytext = yytext.substr(1,yyleng-2); return 'QQ_STRING';"], 28 | [TOKENS.q_string, "yytext = yytext.substr(1,yyleng-2); return 'Q_STRING';"], 29 | ["\\(.+?\\)(?=\\])", "return 'SCRIPT_EXPRESSION'"], 30 | ["\\?\\(.+?\\)(?=\\])", "return 'FILTER_EXPRESSION'"] 31 | ] 32 | }, 33 | 34 | start: "JSON_PATH", 35 | 36 | bnf: { 37 | 38 | JSON_PATH: [ 39 | [ 'DOLLAR', 'yy.ast.set({ expression: { type: "root", value: $1 } }); yy.ast.unshift(); return yy.ast.yield()' ], 40 | [ 'DOLLAR PATH_COMPONENTS', 'yy.ast.set({ expression: { type: "root", value: $1 } }); yy.ast.unshift(); return yy.ast.yield()' ], 41 | [ 'LEADING_CHILD_MEMBER_EXPRESSION', 'yy.ast.unshift(); return yy.ast.yield()' ], 42 | [ 'LEADING_CHILD_MEMBER_EXPRESSION PATH_COMPONENTS', 'yy.ast.set({ operation: "member", scope: "child", expression: { type: "identifier", value: $1 }}); yy.ast.unshift(); return yy.ast.yield()' ] ], 43 | 44 | PATH_COMPONENTS: [ 45 | [ 'PATH_COMPONENT', '' ], 46 | [ 'PATH_COMPONENTS PATH_COMPONENT', '' ] ], 47 | 48 | PATH_COMPONENT: [ 49 | [ 'MEMBER_COMPONENT', 'yy.ast.set({ operation: "member" }); yy.ast.push()' ], 50 | [ 'SUBSCRIPT_COMPONENT', 'yy.ast.set({ operation: "subscript" }); yy.ast.push() ' ] ], 51 | 52 | MEMBER_COMPONENT: [ 53 | [ 'CHILD_MEMBER_COMPONENT', 'yy.ast.set({ scope: "child" })' ], 54 | [ 'DESCENDANT_MEMBER_COMPONENT', 'yy.ast.set({ scope: "descendant" })' ] ], 55 | 56 | CHILD_MEMBER_COMPONENT: [ 57 | [ 'DOT MEMBER_EXPRESSION', '' ] ], 58 | 59 | LEADING_CHILD_MEMBER_EXPRESSION: [ 60 | [ 'MEMBER_EXPRESSION', 'yy.ast.set({ scope: "child", operation: "member" })' ] ], 61 | 62 | DESCENDANT_MEMBER_COMPONENT: [ 63 | [ 'DOT_DOT MEMBER_EXPRESSION', '' ] ], 64 | 65 | MEMBER_EXPRESSION: [ 66 | [ 'STAR', 'yy.ast.set({ expression: { type: "wildcard", value: $1 } })' ], 67 | [ 'IDENTIFIER', 'yy.ast.set({ expression: { type: "identifier", value: $1 } })' ], 68 | [ 'SCRIPT_EXPRESSION', 'yy.ast.set({ expression: { type: "script_expression", value: $1 } })' ], 69 | [ 'INTEGER', 'yy.ast.set({ expression: { type: "numeric_literal", value: parseInt($1) } })' ], 70 | [ 'END', '' ] ], 71 | 72 | SUBSCRIPT_COMPONENT: [ 73 | [ 'CHILD_SUBSCRIPT_COMPONENT', 'yy.ast.set({ scope: "child" })' ], 74 | [ 'DESCENDANT_SUBSCRIPT_COMPONENT', 'yy.ast.set({ scope: "descendant" })' ] ], 75 | 76 | CHILD_SUBSCRIPT_COMPONENT: [ 77 | [ '[ SUBSCRIPT ]', '' ] ], 78 | 79 | DESCENDANT_SUBSCRIPT_COMPONENT: [ 80 | [ 'DOT_DOT [ SUBSCRIPT ]', '' ] ], 81 | 82 | SUBSCRIPT: [ 83 | [ 'SUBSCRIPT_EXPRESSION', '' ], 84 | [ 'SUBSCRIPT_EXPRESSION_LIST', '$1.length > 1? yy.ast.set({ expression: { type: "union", value: $1 } }) : $$ = $1' ] ], 85 | 86 | SUBSCRIPT_EXPRESSION_LIST: [ 87 | [ 'SUBSCRIPT_EXPRESSION_LISTABLE', '$$ = [$1]'], 88 | [ 'SUBSCRIPT_EXPRESSION_LIST , SUBSCRIPT_EXPRESSION_LISTABLE', '$$ = $1.concat($3)' ] ], 89 | 90 | SUBSCRIPT_EXPRESSION_LISTABLE: [ 91 | [ 'INTEGER', '$$ = { expression: { type: "numeric_literal", value: parseInt($1) } }; yy.ast.set($$)' ], 92 | [ 'STRING_LITERAL', '$$ = { expression: { type: "string_literal", value: $1 } }; yy.ast.set($$)' ], 93 | [ 'ARRAY_SLICE', '$$ = { expression: { type: "slice", value: $1 } }; yy.ast.set($$)' ] ], 94 | 95 | SUBSCRIPT_EXPRESSION: [ 96 | [ 'STAR', '$$ = { expression: { type: "wildcard", value: $1 } }; yy.ast.set($$)' ], 97 | [ 'SCRIPT_EXPRESSION', '$$ = { expression: { type: "script_expression", value: $1 } }; yy.ast.set($$)' ], 98 | [ 'FILTER_EXPRESSION', '$$ = { expression: { type: "filter_expression", value: $1 } }; yy.ast.set($$)' ] ], 99 | 100 | STRING_LITERAL: [ 101 | [ 'QQ_STRING', "$$ = $1" ], 102 | [ 'Q_STRING', "$$ = $1" ] ] 103 | }, 104 | moduleInclude: null, 105 | actionInclude: null 106 | }; 107 | 108 | grammar.moduleInclude = fs.readFileSync(path.join(__dirname, '..', 'include', 'module.js')).toString(); 109 | grammar.actionInclude = fs.readFileSync(path.join(__dirname, '..', 'include', 'action.js')).toString(); 110 | 111 | /** 112 | * @internal 113 | * @hidden 114 | */ 115 | export const GRAMMAR = grammar; 116 | -------------------------------------------------------------------------------- /src/handlers.ts: -------------------------------------------------------------------------------- 1 | import * as aesprim from '../upstream/aesprim'; 2 | import * as ESTree from 'estree'; 3 | 4 | import { slice } from './slice'; 5 | import { uniq as _uniq } from 'underscore'; 6 | import { JSONPath } from '.'; 7 | import _evaluate from 'static-eval'; 8 | 9 | /** 10 | * @internal 11 | * @hidden 12 | */ 13 | function traverser(recurse?) { 14 | return function(partial, ref, passable, count) { 15 | 16 | var value = partial.value; 17 | var path = partial.path; 18 | 19 | var results = []; 20 | 21 | var descend = function(value, path) { 22 | 23 | if (is_array(value)) { 24 | value.forEach(function(element, index) { 25 | if (results.length >= count) { return } 26 | if (passable(index, element, ref)) { 27 | results.push({ path: path.concat(index), value: element }); 28 | } 29 | }); 30 | value.forEach(function(element, index) { 31 | if (results.length >= count) { return } 32 | if (recurse) { 33 | descend(element, path.concat(index)); 34 | } 35 | }); 36 | } else if (is_object(value)) { 37 | Object.keys(value).forEach(function(k) { 38 | if (results.length >= count) { return } 39 | if (passable(k, value[k], ref)) { 40 | results.push({ path: path.concat(k), value: value[k] }); 41 | } 42 | }) 43 | Object.keys(value).forEach(function(k) { 44 | if (results.length >= count) { return } 45 | if (recurse) { 46 | descend(value[k], path.concat(k)); 47 | } 48 | }); 49 | } 50 | }.bind(this); 51 | descend(value, path); 52 | return results; 53 | } 54 | } 55 | 56 | /** 57 | * @internal 58 | * @hidden 59 | */ 60 | function evaluate(ast: ESTree.Expression, vars: Record) { 61 | try { 62 | return _evaluate(ast, vars); 63 | } catch (e) { 64 | } 65 | } 66 | 67 | /** 68 | * @internal 69 | * @hidden 70 | */ 71 | function _descend(passable) { 72 | return function(component, partial, count) { 73 | return this.descend(partial, component.expression.value, passable, count); 74 | } 75 | } 76 | 77 | /** 78 | * @internal 79 | * @hidden 80 | */ 81 | function _traverse(passable) { 82 | return function(component, partial, count) { 83 | return this.traverse(partial, component.expression.value, passable, count); 84 | } 85 | } 86 | 87 | /** 88 | * @internal 89 | * @hidden 90 | */ 91 | function unique(results : any[]) { 92 | results = results.filter(d => d); 93 | 94 | return _uniq( 95 | results, 96 | r => r.path.map(function(c) { return String(c).replace('-', '--') }).join('-') 97 | ); 98 | } 99 | 100 | 101 | /** 102 | * @internal 103 | * @hidden 104 | */ 105 | function _parse_nullable_int(val) { 106 | var sval = String(val); 107 | return sval.match(/^-?[0-9]+$/) ? parseInt(sval) : null; 108 | } 109 | 110 | 111 | /** 112 | * @internal 113 | * @hidden 114 | */ 115 | function is_array(val) { 116 | return Array.isArray(val); 117 | } 118 | 119 | 120 | /** 121 | * @internal 122 | * @hidden 123 | */ 124 | function is_object(val) { 125 | // is this a non-array, non-null object? 126 | return val && !(val instanceof Array) && val instanceof Object; 127 | } 128 | 129 | 130 | /** 131 | * @internal 132 | * @hidden 133 | */ 134 | function eval_recurse(partial, src, template) { 135 | 136 | var ast = aesprim.parse(src).body[0].expression; 137 | var value = evaluate(ast, { '@': partial.value }); 138 | var path = template.replace(/\{\{\s*value\s*\}\}/g, value); 139 | 140 | var results = JSONPath.nodes(partial.value, path); 141 | results.forEach(function(r) { 142 | r.path = partial.path.concat(r.path.slice(1)); 143 | }); 144 | 145 | return results; 146 | } 147 | 148 | /** 149 | * @internal 150 | * @hidden 151 | */ 152 | export class Handlers { 153 | constructor() { 154 | this.initialize(); 155 | } 156 | 157 | traverse; 158 | descend; 159 | 160 | private _fns = { 161 | 'member-child-identifier': (component, partial) => { 162 | var key = component.expression.value; 163 | var value = partial.value; 164 | if (value instanceof Object && key in value) { 165 | return [ { value: value[key], path: partial.path.concat(key) } ] 166 | } 167 | }, 168 | 169 | 'member-descendant-identifier': 170 | _traverse(function(key, value, ref) { return key == ref }), 171 | 172 | 'subscript-child-numeric_literal': 173 | _descend(function(key, value, ref) { return key === ref }), 174 | 175 | 'member-child-numeric_literal': 176 | _descend(function(key, value, ref) { return String(key) === String(ref) }), 177 | 178 | 'subscript-descendant-numeric_literal': 179 | _traverse(function(key, value, ref) { return key === ref }), 180 | 181 | 'member-child-wildcard': 182 | _descend(function() { return true }), 183 | 184 | 'member-descendant-wildcard': 185 | _traverse(function() { return true }), 186 | 187 | 'subscript-descendant-wildcard': 188 | _traverse(function() { return true }), 189 | 190 | 'subscript-child-wildcard': 191 | _descend(function() { return true }), 192 | 193 | 'subscript-child-slice': function(component, partial) { 194 | if (is_array(partial.value)) { 195 | var args = component.expression.value.split(':').map(_parse_nullable_int); 196 | var values = partial.value.map(function(v, i) { return { value: v, path: partial.path.concat(i) } }); 197 | return slice.apply(null, [values].concat(args)); 198 | } 199 | }, 200 | 201 | 'subscript-child-union': function(component, partial) { 202 | var results = []; 203 | component.expression.value.forEach(function(component) { 204 | var _component = { operation: 'subscript', scope: 'child', expression: component.expression }; 205 | var handler = this.resolve(_component); 206 | var _results = handler(_component, partial); 207 | if (_results) { 208 | results = results.concat(_results); 209 | } 210 | }, this); 211 | 212 | return unique(results); 213 | }, 214 | 215 | 'subscript-descendant-union': function(component, partial, count) { 216 | var self = this; 217 | 218 | var results = []; 219 | var nodes = JSONPath.nodes(partial, '$..*').slice(1); 220 | 221 | nodes.forEach(function(node) { 222 | if (results.length >= count) return; 223 | component.expression.value.forEach(function(component) { 224 | var _component = { operation: 'subscript', scope: 'child', expression: component.expression }; 225 | var handler = self.resolve(_component); 226 | var _results = handler(_component, node); 227 | results = results.concat(_results); 228 | }); 229 | }); 230 | 231 | return unique(results); 232 | }, 233 | 234 | 'subscript-child-filter_expression': function(component, partial, count) { 235 | 236 | // slice out the expression from ?(expression) 237 | var src = component.expression.value.slice(2, -1); 238 | var ast = aesprim.parse(src).body[0].expression; 239 | 240 | var passable = function(key, value) { 241 | return evaluate(ast, { '@': value }); 242 | } 243 | 244 | return this.descend(partial, null, passable, count); 245 | 246 | }, 247 | 248 | 'subscript-descendant-filter_expression': function(component, partial, count) { 249 | 250 | // slice out the expression from ?(expression) 251 | var src = component.expression.value.slice(2, -1); 252 | var ast = aesprim.parse(src).body[0].expression; 253 | 254 | var passable = function(key, value) { 255 | return evaluate(ast, { '@': value }); 256 | } 257 | 258 | return this.traverse(partial, null, passable, count); 259 | }, 260 | 261 | 'subscript-child-script_expression': function(component, partial) { 262 | var exp = component.expression.value.slice(1, -1); 263 | return eval_recurse(partial, exp, '$[{{value}}]'); 264 | }, 265 | 266 | 'member-child-script_expression': function(component, partial) { 267 | var exp = component.expression.value.slice(1, -1); 268 | return eval_recurse(partial, exp, '$.{{value}}'); 269 | }, 270 | 271 | 'member-descendant-script_expression': function(component, partial) { 272 | var exp = component.expression.value.slice(1, -1); 273 | return eval_recurse(partial, exp, '$..value'); 274 | } 275 | }; 276 | 277 | private initialize() { 278 | this.traverse = traverser(true); 279 | this.descend = traverser(); 280 | 281 | this._fns['subscript-child-string_literal'] = 282 | this._fns['member-child-identifier']; 283 | 284 | this._fns['member-descendant-numeric_literal'] = 285 | this._fns['subscript-descendant-string_literal'] = 286 | this._fns['member-descendant-identifier']; 287 | 288 | } 289 | 290 | resolve(component) { 291 | var key = [ component.operation, component.scope, component.expression.type ].join('-'); 292 | var method = this._fns[key]; 293 | 294 | if (!method) throw new Error("couldn't resolve key: " + key); 295 | return method.bind(this); 296 | } 297 | 298 | register(key, handler) { 299 | if (!(handler instanceof Function)) { 300 | throw new Error("handler must be a function"); 301 | } 302 | 303 | this._fns[key] = handler; 304 | } 305 | } -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './jsonpath'; -------------------------------------------------------------------------------- /src/jsonpath.ts: -------------------------------------------------------------------------------- 1 | import { Handlers } from "./handlers"; 2 | import { TOKENS } from './tokens'; 3 | import { Parser } from './parser'; 4 | 5 | import { assert } from './assert'; 6 | 7 | export class JSONPath { 8 | static parse(string : string) { 9 | assert.ok(typeof string === 'string', "we need a path"); 10 | return new Parser().parse(string); 11 | } 12 | 13 | static parent(obj, string) { 14 | assert.ok(obj instanceof Object, "obj needs to be an object"); 15 | assert.ok(string, "we need a path"); 16 | 17 | let node = this.nodes(obj, string)[0]; 18 | let key = node.path.pop(); /* jshint unused:false */ 19 | return this.value(obj, node.path); 20 | } 21 | 22 | static apply(obj, string, fn) { 23 | 24 | assert.ok(obj instanceof Object, "obj needs to be an object"); 25 | assert.ok(string, "we need a path"); 26 | assert.equal(typeof fn, "function", "fn needs to be function") 27 | 28 | var nodes = this.nodes(obj, string).sort(function(a, b) { 29 | // sort nodes so we apply from the bottom up 30 | return b.path.length - a.path.length; 31 | }); 32 | 33 | nodes.forEach(function(node) { 34 | var key = node.path.pop(); 35 | var parent = this.value(obj, this.stringify(node.path)); 36 | var val = node.value = fn.call(obj, parent[key]); 37 | parent[key] = val; 38 | }, this); 39 | 40 | return nodes; 41 | } 42 | 43 | static value(obj, path, value?) { 44 | 45 | assert.ok(obj instanceof Object, "obj needs to be an object"); 46 | assert.ok(path, "we need a path"); 47 | 48 | if (value !== undefined) { 49 | var node = this.nodes(obj, path).shift(); 50 | if (!node) return this._vivify(obj, path, value); 51 | var key = node.path.slice(-1).shift(); 52 | var parent = this.parent(obj, this.stringify(node.path)); 53 | parent[key] = value; 54 | } 55 | return this.query(obj, this.stringify(path), 1).shift(); 56 | } 57 | 58 | private static _vivify(obj, string, value) { 59 | var self = this; 60 | 61 | assert.ok(obj instanceof Object, "obj needs to be an object"); 62 | assert.ok(string, "we need a path"); 63 | 64 | var path = new Parser().parse(string) 65 | .map(component => component.expression.value); 66 | 67 | var setValue = function(path, value) { 68 | var key = path.pop(); 69 | var node = self.value(obj, path); 70 | if (!node) { 71 | setValue(path.concat(), typeof key === 'string' ? {} : []); 72 | node = self.value(obj, path); 73 | } 74 | node[key] = value; 75 | } 76 | setValue(path, value); 77 | return this.query(obj, string)[0]; 78 | } 79 | 80 | static query(obj : Object, string, count?) { 81 | assert.ok(obj instanceof Object, "obj needs to be an object"); 82 | assert.ok(typeof string === 'string', "we need a path"); 83 | 84 | var results = this.nodes(obj, string, count) 85 | .map(function(r) { return r.value }); 86 | 87 | return results; 88 | } 89 | 90 | static paths(obj, string, count) { 91 | 92 | assert.ok(obj instanceof Object, "obj needs to be an object"); 93 | assert.ok(string, "we need a path"); 94 | 95 | var results = this.nodes(obj, string, count) 96 | .map(function(r) { return r.path }); 97 | 98 | return results; 99 | } 100 | 101 | static nodes(obj, string, count?) { 102 | assert.ok(obj instanceof Object, "obj needs to be an object"); 103 | assert.ok(string, "we need a path"); 104 | 105 | if (count === 0) return []; 106 | 107 | let path = new Parser().parse(string); 108 | let handlers = new Handlers(); 109 | let partials = [ { path: ['$'], value: obj } ]; 110 | let matches = []; 111 | 112 | if (path.length && path[0].expression.type == 'root') path.shift(); 113 | 114 | if (!path.length) return partials; 115 | 116 | path.forEach(function(component, index) { 117 | 118 | if (matches.length >= count) return; 119 | var handler = handlers.resolve(component); 120 | var _partials = []; 121 | 122 | partials.forEach(function(p) { 123 | 124 | if (matches.length >= count) return; 125 | var results = handler(component, p, count); 126 | 127 | if (index == path.length - 1) { 128 | // if we're through the components we're done 129 | matches = matches.concat(results || []); 130 | } else { 131 | // otherwise accumulate and carry on through 132 | _partials = _partials.concat(results || []); 133 | } 134 | }); 135 | 136 | partials = _partials; 137 | 138 | }); 139 | 140 | return count ? matches.slice(0, count) : matches; 141 | } 142 | 143 | static stringify(path) { 144 | assert.ok(path, "we need a path"); 145 | 146 | var string = '$'; 147 | 148 | var templates = { 149 | 'descendant-member': '..{{value}}', 150 | 'child-member': '.{{value}}', 151 | 'descendant-subscript': '..[{{value}}]', 152 | 'child-subscript': '[{{value}}]' 153 | }; 154 | 155 | path = this._normalize(path); 156 | 157 | path.forEach(function(component) { 158 | 159 | if (component.expression.type == 'root') return; 160 | 161 | var key = [component.scope, component.operation].join('-'); 162 | var template = templates[key]; 163 | var value; 164 | 165 | if (component.expression.type == 'string_literal') { 166 | value = JSON.stringify(component.expression.value) 167 | } else { 168 | value = component.expression.value; 169 | } 170 | 171 | if (!template) throw new Error("couldn't find template " + key); 172 | 173 | string += template.replace(/{{value}}/, value); 174 | }); 175 | 176 | return string; 177 | } 178 | 179 | private static _normalize(path) { 180 | assert.ok(path, "we need a path"); 181 | 182 | if (typeof path == "string") { 183 | 184 | return new Parser().parse(path); 185 | 186 | } else if (Array.isArray(path) && typeof path[0] == "string") { 187 | 188 | var _path : any[] = [ { expression: { type: "root", value: "$" } } ]; 189 | 190 | path.forEach(function(component, index) { 191 | 192 | if (component == '$' && index === 0) return; 193 | 194 | if (typeof component == "string" && component.match("^" + TOKENS.identifier + "$")) { 195 | 196 | _path.push({ 197 | operation: 'member', 198 | scope: 'child', 199 | expression: { value: component, type: 'identifier' } 200 | }); 201 | 202 | } else { 203 | 204 | var type = typeof component == "number" ? 205 | 'numeric_literal' : 'string_literal'; 206 | 207 | _path.push({ 208 | operation: 'subscript', 209 | scope: 'child', 210 | expression: { value: component, type: type } 211 | }); 212 | } 213 | }); 214 | 215 | return _path; 216 | 217 | } else if (Array.isArray(path) && typeof path[0] == "object") { 218 | 219 | return path 220 | } 221 | 222 | throw new Error("couldn't understand path " + path); 223 | } 224 | } -------------------------------------------------------------------------------- /src/lessons.test.ts: -------------------------------------------------------------------------------- 1 | import { suite } from 'razmin'; 2 | import * as assert from 'assert'; 3 | import * as fs from 'fs'; 4 | import * as path from 'path'; 5 | import { JSONPath } from './jsonpath'; 6 | 7 | const data = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'fixtures', 'store.json')).toString()); 8 | 9 | suite(describe => { 10 | describe('orig-google-code-issues', it => { 11 | it('comma in eval', () => { 12 | var pathExpression = '$..book[?(@.price && ",")]' 13 | var results = JSONPath.query(data, pathExpression); 14 | assert.deepEqual(results, data.store.book); 15 | }); 16 | 17 | it('member names with dots', () => { 18 | var data = { 'www.google.com': 42, 'www.wikipedia.org': 190 }; 19 | var results = JSONPath.query(data, "$['www.google.com']"); 20 | assert.deepEqual(results, [ 42 ]); 21 | }); 22 | 23 | it('nested objects with filter', () => { 24 | var data = { dataResult: { object: { objectInfo: { className: "folder", typeName: "Standard Folder", id: "uniqueId" } } } }; 25 | var results = JSONPath.query(data, "$..object[?(@.className=='folder')]"); 26 | assert.deepEqual(results, [ data.dataResult.object.objectInfo ]); 27 | }); 28 | 29 | it('script expressions with @ char', () => { 30 | var data = { "DIV": [{ "@class": "value", "val": 5 }] }; 31 | var results = JSONPath.query(data, "$..DIV[?(@['@class']=='value')]"); 32 | assert.deepEqual(results, data.DIV); 33 | }); 34 | 35 | it('negative slices', () => { 36 | var results = JSONPath.query(data, "$..book[-1:].title"); 37 | assert.deepEqual(results, ['The Lord of the Rings']); 38 | }); 39 | 40 | }); 41 | 42 | }); 43 | 44 | -------------------------------------------------------------------------------- /src/parse.test.ts: -------------------------------------------------------------------------------- 1 | import { suite } from 'razmin'; 2 | import * as assert from 'assert'; 3 | import { JSONPath } from './jsonpath'; 4 | 5 | suite(describe => { 6 | describe('parse', it => { 7 | it('should parse root-only', function() { 8 | var path = JSONPath.parse('$'); 9 | assert.deepEqual(path, [ { expression: { type: 'root', value: '$' } } ]); 10 | }); 11 | 12 | it('parse path for store', function() { 13 | var path = JSONPath.parse('$.store'); 14 | assert.deepEqual(path, [ 15 | { expression: { type: 'root', value: '$' } }, 16 | { operation: 'member', scope: 'child', expression: { type: 'identifier', value: 'store' } } 17 | ]) 18 | }); 19 | 20 | it('parse path for the authors of all books in the store', function() { 21 | var path = JSONPath.parse('$.store.book[*].author'); 22 | assert.deepEqual(path, [ 23 | { expression: { type: 'root', value: '$' } }, 24 | { operation: 'member', scope: 'child', expression: { type: 'identifier', value: 'store' } }, 25 | { operation: 'member', scope: 'child', expression: { type: 'identifier', value: 'book' } }, 26 | { operation: 'subscript', scope: 'child', expression: { type: 'wildcard', value: '*' } }, 27 | { operation: 'member', scope: 'child', expression: { type: 'identifier', value: 'author' } } 28 | ]) 29 | }); 30 | 31 | it('parse path for all authors', function() { 32 | var path = JSONPath.parse('$..author'); 33 | assert.deepEqual(path, [ 34 | { expression: { type: 'root', value: '$' } }, 35 | { operation: 'member', scope: 'descendant', expression: { type: 'identifier', value: 'author' } } 36 | ]) 37 | }); 38 | 39 | it('parse path for all authors via subscript descendant string literal', function() { 40 | var path = JSONPath.parse("$..['author']"); 41 | assert.deepEqual(path, [ 42 | { expression: { type: 'root', value: '$' } }, 43 | { operation: 'subscript', scope: 'descendant', expression: { type: 'string_literal', value: 'author' } } 44 | ]) 45 | }); 46 | 47 | it('parse path for all things in store', function() { 48 | var path = JSONPath.parse('$.store.*'); 49 | assert.deepEqual(path, [ 50 | { expression: { type: 'root', value: '$' } }, 51 | { operation: 'member', scope: 'child', expression: { type: 'identifier', value: 'store' } }, 52 | { operation: 'member', scope: 'child', expression: { type: 'wildcard', value: '*' } } 53 | ]) 54 | }); 55 | 56 | it('parse path for price of everything in the store', function() { 57 | var path = JSONPath.parse('$.store..price'); 58 | assert.deepEqual(path, [ 59 | { expression: { type: 'root', value: '$' } }, 60 | { operation: 'member', scope: 'child', expression: { type: 'identifier', value: 'store' } }, 61 | { operation: 'member', scope: 'descendant', expression: { type: 'identifier', value: 'price' } } 62 | ]) 63 | }); 64 | 65 | it('parse path for the last book in order via expression', function() { 66 | var path = JSONPath.parse('$..book[(@.length-1)]'); 67 | assert.deepEqual(path, [ 68 | { expression: { type: 'root', value: '$' } }, 69 | { operation: 'member', scope: 'descendant', expression: { type: 'identifier', value: 'book' } }, 70 | { operation: 'subscript', scope: 'child', expression: { type: 'script_expression', value: '(@.length-1)' } } 71 | ]) 72 | }); 73 | 74 | it('parse path for the first two books via union', function() { 75 | var path = JSONPath.parse('$..book[0,1]'); 76 | 77 | assert.deepEqual(path, [ 78 | { expression: { type: 'root', value: '$' } }, 79 | { operation: 'member', scope: 'descendant', expression: { type: 'identifier', value: 'book' } }, 80 | { operation: 'subscript', scope: 'child', expression: { type: 'union', value: [ { expression: { type: 'numeric_literal', value: '0' } }, { expression: { type: 'numeric_literal', value: '1' } } ] } } 81 | ]) 82 | }); 83 | 84 | it('parse path for the first two books via slice', function() { 85 | var path = JSONPath.parse('$..book[0:2]'); 86 | assert.deepEqual(path, [ 87 | { expression: { type: 'root', value: '$' } }, 88 | { operation: 'member', scope: 'descendant', expression: { type: 'identifier', value: 'book' } }, 89 | { operation: 'subscript', scope: 'child', expression: { type: 'slice', value: '0:2' } } 90 | ]) 91 | }); 92 | 93 | it('parse path to filter all books with isbn number', function() { 94 | var path = JSONPath.parse('$..book[?(@.isbn)]'); 95 | assert.deepEqual(path, [ 96 | { expression: { type: 'root', value: '$' } }, 97 | { operation: 'member', scope: 'descendant', expression: { type: 'identifier', value: 'book' } }, 98 | { operation: 'subscript', scope: 'child', expression: { type: 'filter_expression', value: '?(@.isbn)' } } 99 | ]) 100 | }); 101 | 102 | it('parse path to filter all books with a price less than 10', function() { 103 | var path = JSONPath.parse('$..book[?(@.price<10)]'); 104 | assert.deepEqual(path, [ 105 | { expression: { type: 'root', value: '$' } }, 106 | { operation: 'member', scope: 'descendant', expression: { type: 'identifier', value: 'book' } }, 107 | { operation: 'subscript', scope: 'child', expression: { type: 'filter_expression', value: '?(@.price<10)' } } 108 | ]) 109 | }); 110 | 111 | it('parse path to match all elements', function() { 112 | var path = JSONPath.parse('$..*'); 113 | assert.deepEqual(path, [ 114 | { expression: { type: 'root', value: '$' } }, 115 | { operation: 'member', scope: 'descendant', expression: { type: 'wildcard', value: '*' } } 116 | ]) 117 | }); 118 | 119 | it('parse path with leading member', function() { 120 | var path = JSONPath.parse('store'); 121 | assert.deepEqual(path, [ 122 | { operation: 'member', scope: 'child', expression: { type: 'identifier', value: 'store' } } 123 | ]) 124 | }); 125 | 126 | it('parse path with leading member and followers', function() { 127 | var path = JSONPath.parse('Request.prototype.end'); 128 | assert.deepEqual(path, [ 129 | { operation: 'member', scope: 'child', expression: { type: 'identifier', value: 'Request' } }, 130 | { operation: 'member', scope: 'child', expression: { type: 'identifier', value: 'prototype' } }, 131 | { operation: 'member', scope: 'child', expression: { type: 'identifier', value: 'end' } } 132 | ]) 133 | }); 134 | 135 | it('parser ast is reinitialized after parse() throws', function() { 136 | assert.throws(function() { var path = JSONPath.parse('store.book...') }) 137 | var path = JSONPath.parse('$..price'); 138 | assert.deepEqual(path, [ 139 | { "expression": { "type": "root", "value": "$" } }, 140 | { "expression": { "type": "identifier", "value": "price" }, "operation": "member", "scope": "descendant"} 141 | ]) 142 | }); 143 | 144 | }); 145 | 146 | describe('parse-negative', it => { 147 | it('parse path with leading member component throws', () => { 148 | assert.throws(() => JSONPath.parse('.store'), /Expecting 'DOLLAR'/); 149 | }); 150 | 151 | it('parse path with leading descendant member throws', () => { 152 | assert.throws(function() { var path = JSONPath.parse('..store') }, /Expecting 'DOLLAR'/) 153 | }); 154 | 155 | it('leading script throws', () => { 156 | assert.throws(function() { var path = JSONPath.parse('()') }, /Unrecognized text/) 157 | }); 158 | 159 | it('first time friendly error', () => { 160 | assert.throws(function() { JSONPath.parse('$...') }, /Expecting 'STAR'/) 161 | }); 162 | }); 163 | }); 164 | -------------------------------------------------------------------------------- /src/parser.ts: -------------------------------------------------------------------------------- 1 | import * as gparser from '../generated/parser'; 2 | 3 | /** 4 | * @hidden 5 | * @internal 6 | */ 7 | export function Parser(): void { 8 | let parser = new gparser.Parser(); 9 | let _parseError = parser.parseError; 10 | 11 | parser.yy.parseError = function() { 12 | if (parser.yy.ast) { 13 | parser.yy.ast.initialize(); 14 | } 15 | _parseError.apply(parser, arguments); 16 | } 17 | 18 | return parser; 19 | } -------------------------------------------------------------------------------- /src/query.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | import * as fs from 'fs'; 3 | import * as path from 'path'; 4 | 5 | import { JSONPath } from './jsonpath'; 6 | import { suite, describe } from 'razmin'; 7 | 8 | const data = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'fixtures', 'store.json')).toString()); 9 | 10 | suite(function() { 11 | describe('query', it => { 12 | it('first-level member', function() { 13 | var results = JSONPath.nodes(data, '$.store'); 14 | assert.deepEqual(results, [ { path: ['$', 'store'], value: data.store } ]); 15 | }); 16 | 17 | it('authors of all books in the store', function() { 18 | var results = JSONPath.nodes(data, '$.store.book[*].author'); 19 | assert.deepEqual(results, [ 20 | { path: ['$', 'store', 'book', 0, 'author'], value: 'Nigel Rees' }, 21 | { path: ['$', 'store', 'book', 1, 'author'], value: 'Evelyn Waugh' }, 22 | { path: ['$', 'store', 'book', 2, 'author'], value: 'Herman Melville' }, 23 | { path: ['$', 'store', 'book', 3, 'author'], value: 'J. R. R. Tolkien' } 24 | ]); 25 | }); 26 | 27 | it('all authors', function() { 28 | var results = JSONPath.nodes(data, '$..author'); 29 | assert.deepEqual(results, [ 30 | { path: ['$', 'store', 'book', 0, 'author'], value: 'Nigel Rees' }, 31 | { path: ['$', 'store', 'book', 1, 'author'], value: 'Evelyn Waugh' }, 32 | { path: ['$', 'store', 'book', 2, 'author'], value: 'Herman Melville' }, 33 | { path: ['$', 'store', 'book', 3, 'author'], value: 'J. R. R. Tolkien' } 34 | ]); 35 | }); 36 | 37 | it('all authors via subscript descendant string literal', function() { 38 | var results = JSONPath.nodes(data, "$..['author']"); 39 | assert.deepEqual(results, [ 40 | { path: ['$', 'store', 'book', 0, 'author'], value: 'Nigel Rees' }, 41 | { path: ['$', 'store', 'book', 1, 'author'], value: 'Evelyn Waugh' }, 42 | { path: ['$', 'store', 'book', 2, 'author'], value: 'Herman Melville' }, 43 | { path: ['$', 'store', 'book', 3, 'author'], value: 'J. R. R. Tolkien' } 44 | ]); 45 | }); 46 | 47 | it('all things in store', function() { 48 | var results = JSONPath.nodes(data, '$.store.*'); 49 | assert.deepEqual(results, [ 50 | { path: ['$', 'store', 'book'], value: data.store.book }, 51 | { path: ['$', 'store', 'bicycle'], value: data.store.bicycle } 52 | ]); 53 | }); 54 | 55 | it('price of everything in the store', function() { 56 | var results = JSONPath.nodes(data, '$.store..price'); 57 | assert.deepEqual(results, [ 58 | { path: ['$', 'store', 'book', 0, 'price'], value: 8.95 }, 59 | { path: ['$', 'store', 'book', 1, 'price'], value: 12.99 }, 60 | { path: ['$', 'store', 'book', 2, 'price'], value: 8.99 }, 61 | { path: ['$', 'store', 'book', 3, 'price'], value: 22.99 }, 62 | { path: ['$', 'store', 'bicycle', 'price'], value: 19.95 } 63 | ]); 64 | }); 65 | 66 | it('last book in order via expression', function() { 67 | var results = JSONPath.nodes(data, '$..book[(@.length-1)]'); 68 | assert.deepEqual(results, [ { path: ['$', 'store', 'book', 3], value: data.store.book[3] }]); 69 | }); 70 | 71 | it('first two books via union', function() { 72 | var results = JSONPath.nodes(data, '$..book[0,1]'); 73 | assert.deepEqual(results, [ 74 | { path: ['$', 'store', 'book', 0], value: data.store.book[0] }, 75 | { path: ['$', 'store', 'book', 1], value: data.store.book[1] } 76 | ]); 77 | }); 78 | 79 | it('first two books via slice', function() { 80 | var results = JSONPath.nodes(data, '$..book[0:2]'); 81 | assert.deepEqual(results, [ 82 | { path: ['$', 'store', 'book', 0], value: data.store.book[0] }, 83 | { path: ['$', 'store', 'book', 1], value: data.store.book[1] } 84 | ]); 85 | }); 86 | 87 | it('filter all books with isbn number', function() { 88 | var results = JSONPath.nodes(data, '$..book[?(@.isbn)]'); 89 | assert.deepEqual(results, [ 90 | { path: ['$', 'store', 'book', 2], value: data.store.book[2] }, 91 | { path: ['$', 'store', 'book', 3], value: data.store.book[3] } 92 | ]); 93 | }); 94 | 95 | it('filter all books with a price less than 10', function() { 96 | var results = JSONPath.nodes(data, '$..book[?(@.price<10)]'); 97 | assert.deepEqual(results, [ 98 | { path: ['$', 'store', 'book', 0], value: data.store.book[0] }, 99 | { path: ['$', 'store', 'book', 2], value: data.store.book[2] } 100 | ]); 101 | }); 102 | 103 | it('first ten of all elements', function() { 104 | var results = JSONPath.nodes(data, '$..*', 10); 105 | assert.deepEqual(results, [ 106 | { path: [ '$', 'store' ], value: data.store }, 107 | { path: [ '$', 'store', 'book' ], value: data.store.book }, 108 | { path: [ '$', 'store', 'bicycle' ], value: data.store.bicycle }, 109 | { path: [ '$', 'store', 'book', 0 ], value: data.store.book[0] }, 110 | { path: [ '$', 'store', 'book', 1 ], value: data.store.book[1] }, 111 | { path: [ '$', 'store', 'book', 2 ], value: data.store.book[2] }, 112 | { path: [ '$', 'store', 'book', 3 ], value: data.store.book[3] }, 113 | { path: [ '$', 'store', 'book', 0, 'category' ], value: 'reference' }, 114 | { path: [ '$', 'store', 'book', 0, 'author' ], value: 'Nigel Rees' }, 115 | { path: [ '$', 'store', 'book', 0, 'title' ], value: 'Sayings of the Century' } 116 | ]) 117 | }); 118 | 119 | it('all elements', function() { 120 | var results = JSONPath.nodes(data, '$..*'); 121 | 122 | assert.deepEqual(results, [ 123 | { path: [ '$', 'store' ], value: data.store }, 124 | { path: [ '$', 'store', 'book' ], value: data.store.book }, 125 | { path: [ '$', 'store', 'bicycle' ], value: data.store.bicycle }, 126 | { path: [ '$', 'store', 'book', 0 ], value: data.store.book[0] }, 127 | { path: [ '$', 'store', 'book', 1 ], value: data.store.book[1] }, 128 | { path: [ '$', 'store', 'book', 2 ], value: data.store.book[2] }, 129 | { path: [ '$', 'store', 'book', 3 ], value: data.store.book[3] }, 130 | { path: [ '$', 'store', 'book', 0, 'category' ], value: 'reference' }, 131 | { path: [ '$', 'store', 'book', 0, 'author' ], value: 'Nigel Rees' }, 132 | { path: [ '$', 'store', 'book', 0, 'title' ], value: 'Sayings of the Century' }, 133 | { path: [ '$', 'store', 'book', 0, 'price' ], value: 8.95 }, 134 | { path: [ '$', 'store', 'book', 1, 'category' ], value: 'fiction' }, 135 | { path: [ '$', 'store', 'book', 1, 'author' ], value: 'Evelyn Waugh' }, 136 | { path: [ '$', 'store', 'book', 1, 'title' ], value: 'Sword of Honour' }, 137 | { path: [ '$', 'store', 'book', 1, 'price' ], value: 12.99 }, 138 | { path: [ '$', 'store', 'book', 2, 'category' ], value: 'fiction' }, 139 | { path: [ '$', 'store', 'book', 2, 'author' ], value: 'Herman Melville' }, 140 | { path: [ '$', 'store', 'book', 2, 'title' ], value: 'Moby Dick' }, 141 | { path: [ '$', 'store', 'book', 2, 'isbn' ], value: '0-553-21311-3' }, 142 | { path: [ '$', 'store', 'book', 2, 'price' ], value: 8.99 }, 143 | { path: [ '$', 'store', 'book', 3, 'category' ], value: 'fiction' }, 144 | { path: [ '$', 'store', 'book', 3, 'author' ], value: 'J. R. R. Tolkien' }, 145 | { path: [ '$', 'store', 'book', 3, 'title' ], value: 'The Lord of the Rings' }, 146 | { path: [ '$', 'store', 'book', 3, 'isbn' ], value: '0-395-19395-8' }, 147 | { path: [ '$', 'store', 'book', 3, 'price' ], value: 22.99 }, 148 | { path: [ '$', 'store', 'bicycle', 'color' ], value: 'red' }, 149 | { path: [ '$', 'store', 'bicycle', 'price' ], value: 19.95 } 150 | ]); 151 | }); 152 | 153 | it('all elements via subscript wildcard', function() { 154 | var results = JSONPath.nodes(data, '$..*'); 155 | assert.deepEqual(JSONPath.nodes(data, '$..[*]'), JSONPath.nodes(data, '$..*')); 156 | }); 157 | 158 | it('object subscript wildcard', function() { 159 | var results = JSONPath.query(data, '$.store[*]'); 160 | assert.deepEqual(results, [ data.store.book, data.store.bicycle ]); 161 | }); 162 | 163 | it('no match returns empty array', function() { 164 | var results = JSONPath.nodes(data, '$..bookz'); 165 | assert.deepEqual(results, []); 166 | }); 167 | 168 | it('member numeric literal gets first element', function() { 169 | var results = JSONPath.nodes(data, '$.store.book.0'); 170 | assert.deepEqual(results, [ { path: [ '$', 'store', 'book', 0 ], value: data.store.book[0] } ]); 171 | }); 172 | 173 | it('member numeric literal matches string-numeric key', function() { 174 | var data = { authors: { '1': 'Herman Melville', '2': 'J. R. R. Tolkien' } }; 175 | var results = JSONPath.nodes(data, '$.authors.1'); 176 | assert.deepEqual(results, [ { path: [ '$', 'authors', 1 ], value: 'Herman Melville' } ]); 177 | }); 178 | 179 | it('descendant numeric literal gets first element', function() { 180 | var results = JSONPath.nodes(data, '$.store.book..0'); 181 | assert.deepEqual(results, [ { path: [ '$', 'store', 'book', 0 ], value: data.store.book[0] } ]); 182 | }); 183 | 184 | it('root element gets us original obj', function() { 185 | var results = JSONPath.nodes(data, '$'); 186 | assert.deepEqual(results, [ { path: ['$'], value: data } ]); 187 | }); 188 | 189 | it('subscript double-quoted string', function() { 190 | var results = JSONPath.nodes(data, '$["store"]'); 191 | assert.deepEqual(results, [ { path: ['$', 'store'], value: data.store} ]); 192 | }); 193 | 194 | it('subscript single-quoted string', function() { 195 | var results = JSONPath.nodes(data, "$['store']"); 196 | assert.deepEqual(results, [ { path: ['$', 'store'], value: data.store} ]); 197 | }); 198 | 199 | it('leading member component', function() { 200 | var results = JSONPath.nodes(data, "store"); 201 | assert.deepEqual(results, [ { path: ['$', 'store'], value: data.store} ]); 202 | }); 203 | 204 | it('union of three array slices', function() { 205 | var results = JSONPath.query(data, "$.store.book[0:1,1:2,2:3]"); 206 | assert.deepEqual(results, data.store.book.slice(0,3)); 207 | }); 208 | 209 | it('slice with step > 1', function() { 210 | var results = JSONPath.query(data, "$.store.book[0:4:2]"); 211 | assert.deepEqual(results, [ data.store.book[0], data.store.book[2]]); 212 | }); 213 | 214 | it('union of subscript string literal keys', function() { 215 | var results = JSONPath.nodes(data, "$.store['book','bicycle']"); 216 | assert.deepEqual(results, [ 217 | { path: ['$', 'store', 'book'], value: data.store.book }, 218 | { path: ['$', 'store', 'bicycle'], value: data.store.bicycle }, 219 | ]); 220 | }); 221 | 222 | it('union of subscript string literal three keys', function() { 223 | var results = JSONPath.nodes(data, "$.store.book[0]['title','author','price']"); 224 | assert.deepEqual(results, [ 225 | { path: ['$', 'store', 'book', 0, 'title'], value: data.store.book[0].title }, 226 | { path: ['$', 'store', 'book', 0, 'author'], value: data.store.book[0].author }, 227 | { path: ['$', 'store', 'book', 0, 'price'], value: data.store.book[0].price } 228 | ]); 229 | }); 230 | 231 | it('union of subscript integer three keys followed by member-child-identifier', function() { 232 | var results = JSONPath.nodes(data, "$.store.book[1,2,3]['title']"); 233 | assert.deepEqual(results, [ 234 | { path: ['$', 'store', 'book', 1, 'title'], value: data.store.book[1].title }, 235 | { path: ['$', 'store', 'book', 2, 'title'], value: data.store.book[2].title }, 236 | { path: ['$', 'store', 'book', 3, 'title'], value: data.store.book[3].title } 237 | ]); 238 | }); 239 | 240 | it('union of subscript integer three keys followed by union of subscript string literal three keys', function() { 241 | var results = JSONPath.nodes(data, "$.store.book[0,1,2,3]['title','author','price']"); 242 | assert.deepEqual(results, [ 243 | { path: ['$', 'store', 'book', 0, 'title'], value: data.store.book[0].title }, 244 | { path: ['$', 'store', 'book', 0, 'author'], value: data.store.book[0].author }, 245 | { path: ['$', 'store', 'book', 0, 'price'], value: data.store.book[0].price }, 246 | { path: ['$', 'store', 'book', 1, 'title'], value: data.store.book[1].title }, 247 | { path: ['$', 'store', 'book', 1, 'author'], value: data.store.book[1].author }, 248 | { path: ['$', 'store', 'book', 1, 'price'], value: data.store.book[1].price }, 249 | { path: ['$', 'store', 'book', 2, 'title'], value: data.store.book[2].title }, 250 | { path: ['$', 'store', 'book', 2, 'author'], value: data.store.book[2].author }, 251 | { path: ['$', 'store', 'book', 2, 'price'], value: data.store.book[2].price }, 252 | { path: ['$', 'store', 'book', 3, 'title'], value: data.store.book[3].title }, 253 | { path: ['$', 'store', 'book', 3, 'author'], value: data.store.book[3].author }, 254 | { path: ['$', 'store', 'book', 3, 'price'], value: data.store.book[3].price } 255 | ]); 256 | }); 257 | 258 | it('union of subscript integer four keys, including an inexistent one, followed by union of subscript string literal three keys', function() { 259 | var results = JSONPath.nodes(data, "$.store.book[0,1,2,3,151]['title','author','price']"); 260 | assert.deepEqual(results, [ 261 | { path: ['$', 'store', 'book', 0, 'title'], value: data.store.book[0].title }, 262 | { path: ['$', 'store', 'book', 0, 'author'], value: data.store.book[0].author }, 263 | { path: ['$', 'store', 'book', 0, 'price'], value: data.store.book[0].price }, 264 | { path: ['$', 'store', 'book', 1, 'title'], value: data.store.book[1].title }, 265 | { path: ['$', 'store', 'book', 1, 'author'], value: data.store.book[1].author }, 266 | { path: ['$', 'store', 'book', 1, 'price'], value: data.store.book[1].price }, 267 | { path: ['$', 'store', 'book', 2, 'title'], value: data.store.book[2].title }, 268 | { path: ['$', 'store', 'book', 2, 'author'], value: data.store.book[2].author }, 269 | { path: ['$', 'store', 'book', 2, 'price'], value: data.store.book[2].price }, 270 | { path: ['$', 'store', 'book', 3, 'title'], value: data.store.book[3].title }, 271 | { path: ['$', 'store', 'book', 3, 'author'], value: data.store.book[3].author }, 272 | { path: ['$', 'store', 'book', 3, 'price'], value: data.store.book[3].price } 273 | ]); 274 | }); 275 | 276 | it('union of subscript integer three keys followed by union of subscript string literal three keys, followed by inexistent literal key', function() { 277 | var results = JSONPath.nodes(data, "$.store.book[0,1,2,3]['title','author','price','fruit']"); 278 | assert.deepEqual(results, [ 279 | { path: ['$', 'store', 'book', 0, 'title'], value: data.store.book[0].title }, 280 | { path: ['$', 'store', 'book', 0, 'author'], value: data.store.book[0].author }, 281 | { path: ['$', 'store', 'book', 0, 'price'], value: data.store.book[0].price }, 282 | { path: ['$', 'store', 'book', 1, 'title'], value: data.store.book[1].title }, 283 | { path: ['$', 'store', 'book', 1, 'author'], value: data.store.book[1].author }, 284 | { path: ['$', 'store', 'book', 1, 'price'], value: data.store.book[1].price }, 285 | { path: ['$', 'store', 'book', 2, 'title'], value: data.store.book[2].title }, 286 | { path: ['$', 'store', 'book', 2, 'author'], value: data.store.book[2].author }, 287 | { path: ['$', 'store', 'book', 2, 'price'], value: data.store.book[2].price }, 288 | { path: ['$', 'store', 'book', 3, 'title'], value: data.store.book[3].title }, 289 | { path: ['$', 'store', 'book', 3, 'author'], value: data.store.book[3].author }, 290 | { path: ['$', 'store', 'book', 3, 'price'], value: data.store.book[3].price } 291 | ]); 292 | }); 293 | 294 | it('union of subscript 4 array slices followed by union of subscript string literal three keys', function() { 295 | var results = JSONPath.nodes(data, "$.store.book[0:1,1:2,2:3,3:4]['title','author','price']"); 296 | assert.deepEqual(results, [ 297 | { path: ['$', 'store', 'book', 0, 'title'], value: data.store.book[0].title }, 298 | { path: ['$', 'store', 'book', 0, 'author'], value: data.store.book[0].author }, 299 | { path: ['$', 'store', 'book', 0, 'price'], value: data.store.book[0].price }, 300 | { path: ['$', 'store', 'book', 1, 'title'], value: data.store.book[1].title }, 301 | { path: ['$', 'store', 'book', 1, 'author'], value: data.store.book[1].author }, 302 | { path: ['$', 'store', 'book', 1, 'price'], value: data.store.book[1].price }, 303 | { path: ['$', 'store', 'book', 2, 'title'], value: data.store.book[2].title }, 304 | { path: ['$', 'store', 'book', 2, 'author'], value: data.store.book[2].author }, 305 | { path: ['$', 'store', 'book', 2, 'price'], value: data.store.book[2].price }, 306 | { path: ['$', 'store', 'book', 3, 'title'], value: data.store.book[3].title }, 307 | { path: ['$', 'store', 'book', 3, 'author'], value: data.store.book[3].author }, 308 | { path: ['$', 'store', 'book', 3, 'price'], value: data.store.book[3].price } 309 | ]); 310 | }); 311 | 312 | 313 | it('nested parentheses eval', function() { 314 | var pathExpression = '$..book[?( @.price && (@.price + 20 || false) )]' 315 | var results = JSONPath.query(data, pathExpression); 316 | assert.deepEqual(results, data.store.book); 317 | }); 318 | 319 | it('array indexes from 0 to 100', function() { 320 | var data = []; 321 | for (var i = 0; i <= 100; ++i) 322 | data[i] = Math.random(); 323 | 324 | for (var i = 0; i <= 100; ++i) { 325 | var results = JSONPath.query(data, '$[' + i.toString() + ']'); 326 | assert.deepEqual(results, [data[i]]); 327 | } 328 | }); 329 | 330 | it('descendant subscript numeric literal', function() { 331 | var data = [ 0, [ 1, 2, 3 ], [ 4, 5, 6 ] ]; 332 | var results = JSONPath.query(data, '$..[0]'); 333 | assert.deepEqual(results, [ 0, 1, 4 ]); 334 | }); 335 | 336 | it('descendant subscript numeric literal', function() { 337 | var data = [ 0, 1, [ 2, 3, 4 ], [ 5, 6, 7, [ 8, 9 , 10 ] ] ]; 338 | var results = JSONPath.query(data, '$..[0,1]'); 339 | assert.deepEqual(results, [ 0, 1, 2, 3, 5, 6, 8, 9 ]); 340 | }); 341 | 342 | it('throws for no input', function() { 343 | assert.throws(function() { JSONPath.query(123, '$') }, /needs to be an object/); 344 | }); 345 | 346 | it('throws for bad input', function() { 347 | assert.throws(function() { JSONPath.query("string", "string") }, /needs to be an object/); 348 | }); 349 | 350 | it('throws for bad input', function() { 351 | assert.throws(function() { JSONPath.query({}, null) }, /we need a path/); 352 | }); 353 | 354 | it('throws for bad input', function() { 355 | assert.throws(function() { JSONPath.query({}, 42) }, /we need a path/); 356 | }); 357 | 358 | it('union on objects', function() { 359 | assert.deepEqual(JSONPath.query({a: 1, b: 2, c: null}, '$..["a","b","c","d"]'), [1, 2, null]); 360 | }); 361 | }); 362 | }); 363 | 364 | -------------------------------------------------------------------------------- /src/slice.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | 3 | import { suite } from 'razmin'; 4 | import { slice } from './slice'; 5 | 6 | var data = ['a', 'b', 'c', 'd', 'e', 'f']; 7 | 8 | suite(describe => { 9 | describe('slice', it => { 10 | it('no params yields copy', () => { 11 | assert.deepEqual(slice(data), data); 12 | }); 13 | 14 | it('no end param defaults to end', () => { 15 | assert.deepEqual(slice(data, 2), data.slice(2)); 16 | }); 17 | 18 | it('zero end param yields empty', () => { 19 | assert.deepEqual(slice(data, 0, 0), []); 20 | }); 21 | 22 | it('first element with explicit params', () => { 23 | assert.deepEqual(slice(data, 0, 1, 1), ['a']); 24 | }); 25 | 26 | it('last element with explicit params', () => { 27 | assert.deepEqual(slice(data, -1, 6), ['f']); 28 | }); 29 | 30 | it('empty extents and negative step reverses', () => { 31 | assert.deepEqual(slice(data, null, null, -1), ['f', 'e', 'd', 'c', 'b', 'a']); 32 | }); 33 | 34 | it('negative step partial slice', () => { 35 | assert.deepEqual(slice(data, 4, 2, -1), ['e', 'd']); 36 | }); 37 | 38 | it('negative step partial slice no start defaults to end', () => { 39 | assert.deepEqual(slice(data, null, 2, -1), ['f', 'e', 'd']); 40 | }); 41 | 42 | it('extents clamped end', () => { 43 | assert.deepEqual(slice(data, null, 100), data); 44 | }); 45 | 46 | it('extents clamped beginning', () => { 47 | assert.deepEqual(slice(data, -100, 100), data); 48 | }); 49 | 50 | it('backwards extents yields empty', () => { 51 | assert.deepEqual(slice(data, 2, 1), []); 52 | }); 53 | 54 | it('zero step gets shot down', () => { 55 | assert.throws(() => { slice(data, null, null, 0) }); 56 | }); 57 | }); 58 | }); 59 | 60 | -------------------------------------------------------------------------------- /src/slice.ts: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * @internal 4 | * @hidden 5 | */ 6 | export function slice(arr : any[], start? : number, end? : number, step? : number) { 7 | function integer(val) { 8 | return String(val).match(/^[0-9]+$/) ? parseInt(val) : 9 | Number.isFinite(val) ? parseInt(val, 10) : 0; 10 | } 11 | 12 | if (typeof start == 'string') throw new Error("start cannot be a string"); 13 | if (typeof end == 'string') throw new Error("end cannot be a string"); 14 | if (typeof step == 'string') throw new Error("step cannot be a string"); 15 | 16 | var len = arr.length; 17 | 18 | if (step === 0) throw new Error("step cannot be zero"); 19 | step = step ? integer(step) : 1; 20 | 21 | // normalize negative values 22 | start = start < 0 ? len + start : start; 23 | end = end < 0 ? len + end : end; 24 | 25 | // default extents to extents 26 | start = integer(start === 0 ? 0 : !start ? (step > 0 ? 0 : len - 1) : start); 27 | end = integer(end === 0 ? 0 : !end ? (step > 0 ? len : -1) : end); 28 | 29 | // clamp extents 30 | start = step > 0 ? Math.max(0, start) : Math.min(len, start); 31 | end = step > 0 ? Math.min(end, len) : Math.max(-1, end); 32 | 33 | // return empty if extents are backwards 34 | if (step > 0 && end <= start) return []; 35 | if (step < 0 && start <= end) return []; 36 | 37 | var result = []; 38 | 39 | for (var i = start; i != end; i += step) { 40 | if ((step < 0 && i <= end) || (step > 0 && i >= end)) break; 41 | result.push(arr[i]); 42 | } 43 | 44 | return result; 45 | } 46 | -------------------------------------------------------------------------------- /src/stringify.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | 3 | import { suite, describe } from 'razmin'; 4 | import { JSONPath } from './jsonpath'; 5 | 6 | suite(function() { 7 | describe('stringify', it => { 8 | it('simple path stringifies', function() { 9 | var string = JSONPath.stringify(['$', 'a', 'b', 'c']); 10 | assert.equal(string, '$.a.b.c'); 11 | }); 12 | 13 | it('numeric literals end up as subscript numbers', function() { 14 | var string = JSONPath.stringify(['$', 'store', 'book', 0, 'author']); 15 | assert.equal(string, '$.store.book[0].author'); 16 | }); 17 | 18 | it('simple path with no leading root stringifies', function() { 19 | var string = JSONPath.stringify(['a', 'b', 'c']); 20 | assert.equal(string, '$.a.b.c'); 21 | }); 22 | 23 | it('simple parsed path stringifies', function() { 24 | var path = [ 25 | { scope: 'child', operation: 'member', expression: { type: 'identifier', value: 'a' } }, 26 | { scope: 'child', operation: 'member', expression: { type: 'identifier', value: 'b' } }, 27 | { scope: 'child', operation: 'member', expression: { type: 'identifier', value: 'c' } } 28 | ]; 29 | var string = JSONPath.stringify(path); 30 | assert.equal(string, '$.a.b.c'); 31 | }); 32 | 33 | it('keys with hyphens get subscripted', function() { 34 | var string = JSONPath.stringify(['$', 'member-search']); 35 | assert.equal(string, '$["member-search"]'); 36 | }); 37 | 38 | it('complicated path round trips', function() { 39 | var pathExpression = '$..*[0:2].member["string-xyz"]'; 40 | var path = JSONPath.parse(pathExpression); 41 | var string = JSONPath.stringify(path); 42 | assert.equal(string, pathExpression); 43 | }); 44 | 45 | it('complicated path with filter exp round trips', function() { 46 | var pathExpression = '$..*[0:2].member[?(@.val > 10)]'; 47 | var path = JSONPath.parse(pathExpression); 48 | var string = JSONPath.stringify(path); 49 | assert.equal(string, pathExpression); 50 | }); 51 | 52 | it('throws for no input', function() { 53 | assert.throws(function() { JSONPath.stringify(undefined) }, /we need a path/); 54 | }); 55 | }); 56 | }); 57 | -------------------------------------------------------------------------------- /src/sugar.test.ts: -------------------------------------------------------------------------------- 1 | import { suite, describe } from 'razmin'; 2 | import * as assert from 'assert'; 3 | 4 | import { JSONPath } from './jsonpath'; 5 | 6 | suite(function() { 7 | describe('sugar', it => { 8 | it('parent gets us parent value', function() { 9 | let data = { a: 1, b: 2, c: 3, z: { a: 100, b: 200 } }; 10 | let parent = JSONPath.parent(data, '$.z.b'); 11 | assert.equal(parent, data.z); 12 | }); 13 | 14 | it('apply method sets values', function() { 15 | let data = { a: 1, b: 2, c: 3, z: { a: 100, b: 200 } }; 16 | JSONPath.apply(data, '$..a', function(v) { return v + 1 }); 17 | assert.equal(data.a, 2); 18 | assert.equal(data.z.a, 101); 19 | }); 20 | 21 | it('apply method applies survives structural changes', function() { 22 | let data = {a: {b: [1, {c: [2,3]}]}}; 23 | JSONPath.apply(data, '$..*[?(@.length > 1)]', function(array) { 24 | return array.reverse(); 25 | }); 26 | assert.deepEqual(data.a.b, [{c: [3, 2]}, 1]); 27 | }); 28 | 29 | it('value method gets us a value', function() { 30 | var data = { a: 1, b: 2, c: 3, z: { a: 100, b: 200 } }; 31 | var b = JSONPath.value(data, '$..b') 32 | assert.equal(b, data.b); 33 | }); 34 | 35 | it('value method sets us a value', function() { 36 | let data = { a: 1, b: 2, c: 3, z: { a: 100, b: 200 } }; 37 | let b = JSONPath.value(data, '$..b', '5000') 38 | assert.equal(b, 5000); 39 | assert.equal(data.b, 5000); 40 | }); 41 | 42 | it('value method sets new key and value', function() { 43 | let data : any = {}; 44 | let a = JSONPath.value(data, '$.a', 1); 45 | let c = JSONPath.value(data, '$.b.c', 2); 46 | assert.equal(a, 1); 47 | assert.equal(data.a, 1); 48 | assert.equal(c, 2); 49 | assert.equal(data.b.c, 2); 50 | }); 51 | 52 | it('value method sets new array value', function() { 53 | let data : any = {}; 54 | let v1 = JSONPath.value(data, '$.a.d[0]', 4); 55 | let v2 = JSONPath.value(data, '$.a.d[1]', 5); 56 | assert.equal(v1, 4); 57 | assert.equal(v2, 5); 58 | assert.deepEqual(data.a.d, [4, 5]); 59 | }); 60 | 61 | it('value method sets non-literal key', function() { 62 | let data = { "list": [ { "index": 0, "value": "default" }, { "index": 1, "value": "default" } ] }; 63 | JSONPath.value(data, '$.list[?(@.index == 1)].value', "test"); 64 | assert.equal(data.list[1].value, "test"); 65 | }); 66 | 67 | it('paths with a count gets us back count many paths', function() { 68 | let data = [ { a: [ 1, 2, 3 ], b: [ -1, -2, -3 ] }, { } ] 69 | let paths = JSONPath.paths(data, '$..*', 3) 70 | assert.deepEqual(paths, [ ['$', '0'], ['$', '1'], ['$', '0', 'a'] ]); 71 | }); 72 | }); 73 | }); 74 | -------------------------------------------------------------------------------- /src/test.ts: -------------------------------------------------------------------------------- 1 | import { suite } from 'razmin'; 2 | 3 | suite() 4 | .include(['./**/*.test.js']) 5 | .run() 6 | ; -------------------------------------------------------------------------------- /src/tokens.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Defines the token types of JSONPath by regular expression. 3 | * @internal 4 | * @hidden 5 | */ 6 | export const TOKENS = { 7 | identifier: "[a-zA-Z_]+[a-zA-Z0-9_]*", 8 | integer: "-?(?:0|[1-9][0-9]*)", 9 | qq_string: "\"(?:\\\\[\"bfnrt/\\\\]|\\\\u[a-fA-F0-9]{4}|[^\"\\\\])*\"", 10 | q_string: "'(?:\\\\[\'bfnrt/\\\\]|\\\\u[a-fA-F0-9]{4}|[^\'\\\\])*'" 11 | }; 12 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": true, 4 | "module": "commonjs", 5 | "moduleResolution": "node", 6 | "outDir": "./dist", 7 | "esModuleInterop": true, 8 | "sourceMap": true, 9 | "target": "es5", 10 | "lib": [ 11 | "es2016", 12 | "dom" 13 | ] 14 | }, 15 | "include": [ 16 | "./src/**/*.ts" 17 | ], 18 | "typedocOptions": { 19 | "mode": "file", 20 | "out": "docs", 21 | "target": "ES5", 22 | "excludeExternals": true, 23 | "excludePrivate": true, 24 | "excludeNotExported": true, 25 | "externalPattern": "**/node_modules/**", 26 | "exclude": ["**/node_modules/**", "**/*.test.ts"], 27 | "categoryOrder": [ 28 | "Entrypoint", 29 | "*" 30 | ] 31 | } 32 | } --------------------------------------------------------------------------------