diff --git a/Changelog.md b/Changelog.md new file mode 100644 index 0000000..70e1681 --- /dev/null +++ b/Changelog.md @@ -0,0 +1,6 @@ +## Changelog + +### 0.2.8 (Nov 1. 2016) + +- Added `filter` function to remove elements conditionally. +- Updated tests and Readme with `filter` function. \ No newline at end of file diff --git a/README.md b/README.md index 28585df..d4a9794 100755 --- a/README.md +++ b/README.md @@ -153,11 +153,11 @@ Returns the parent of the first matching element. #### jp.apply(obj, pathExpression, fn) -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. +Runs the supplied application 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 and a context object with the `parent` and `key`. Returns matching nodes with their updated values. ```javascript -var nodes = jp.apply(data, '$..author', function(value) { return value.toUpperCase() }); +var nodes = jp.apply(data, '$..author', function(value, ctx) { return value.toUpperCase() }); // [ // { path: ['$', 'store', 'book', 0, 'author'], value: 'NIGEL REES' }, // { path: ['$', 'store', 'book', 1, 'author'], value: 'EVELYN WAUGH' }, @@ -166,6 +166,55 @@ var nodes = jp.apply(data, '$..author', function(value) { return value.toUpperCa // ] ``` +#### jp.filter(obj, pathExpression, fn) + +Filters the data by removing the matched value if the callback returns a truthy value. + +Sample data: + +```js +var data = { + a: [ + { + id: 'book', + price: 100 + }, + { + id: 'car', + price: 3456 + } + ], + b: { + c: 2, + d: 1 + } +}; +``` + +Example: Using `filter` to remove element from an `Array` or `Object` parent node. + +```js +// Remove the first element in `a` +jp.filter(data, '$..a[0]', function(v, ctx) { + return true; +}); + +// Remove the element at 'c' from object b +jp.filter(data, '$..b.c', function(v, ctx) { + return true; +}); + +console.log(data) +// { +// a: [ +// { id: 'car', price: 3456 } +// ], +// b: { +// c: 2 +// } +// } +``` + #### jp.parse(pathExpression) Parse the provided JSONPath expression into path components and their associated operations. diff --git a/lib/index.js b/lib/index.js index 8f5a832..a0c7246 100755 --- a/lib/index.js +++ b/lib/index.js @@ -27,6 +27,33 @@ JSONPath.prototype.parent = function(obj, string) { return this.value(obj, node.path); } +JSONPath.prototype.filter = function(obj, string, fn) { + assert.ok(obj instanceof Object, "obj needs to be an object"); + assert.ok(string, "we need a path"); + assert.equal(typeof fn, "function", "fn needs to be function") + + var nodes = this.nodes(obj, string).sort(function(a, b) { + // sort nodes so we apply from the bottom up + return b.path.length - a.path.length; + }); + + nodes.forEach(function(node) { + var key = node.path.pop(); + var parent = this.value(obj, this.stringify(node.path)); + var val = node.value = fn.call(obj, parent[key], {parent: parent, key: key}); + + if (!val) return; + + if (Array.isArray(parent)) { + parent.splice(key, 1); + return; + } + delete parent[key]; + }, this); + + return nodes; +} + JSONPath.prototype.apply = function(obj, string, fn) { assert.ok(obj instanceof Object, "obj needs to be an object"); @@ -41,7 +68,7 @@ JSONPath.prototype.apply = function(obj, string, fn) { nodes.forEach(function(node) { var key = node.path.pop(); var parent = this.value(obj, this.stringify(node.path)); - var val = node.value = fn.call(obj, parent[key]); + var val = node.value = fn.call(obj, parent[key], {parent: parent, key: key}); parent[key] = val; }, this); diff --git a/package.json b/package.json index ed180f6..4f12f2a 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "jsonpath", "description": "Query JavaScript objects with JSONPath expressions. Robust / safe JSONPath engine for Node.js.", - "version": "0.2.7", + "version": "0.2.8", "author": "david@fmail.co.uk", "scripts": { "postinstall": "node lib/aesprim.js > generated/aesprim-browser.js", @@ -9,10 +9,10 @@ "generate": "node bin/generate_parser.js > generated/parser.js" }, "dependencies": { - "esprima": "1.2.2", - "jison": "0.4.13", - "static-eval": "0.2.3", - "underscore": "1.7.0" + "esprima": "^2.0.0", + "jison": "^0.4.17", + "static-eval": "^1.1.1", + "underscore": "^1.8.3" }, "browser": { "./lib/aesprim.js": "./generated/aesprim-browser.js" @@ -24,7 +24,7 @@ "grunt-contrib-uglify": "0.9.1", "jscs": "1.10.0", "jshint": "2.6.0", - "mocha": "2.1.0" + "mocha": "^3.1.0" }, "repository": { "type": "git", diff --git a/test/sugar.js b/test/sugar.js index 98a21b9..ca07b43 100644 --- a/test/sugar.js +++ b/test/sugar.js @@ -17,6 +17,54 @@ suite('sugar', function() { assert.equal(data.z.a, 101); }); + test('filter method deletes object from Array specified by remove function', function() { + var data = { + a: [ + { + id: 'book', + price: 100 + }, + { + id: 'car', + price: 3456 + } + ], + b: 2 + }; + + jp.filter(data, '$..a[0]', function(v) { + return true; + }); + + assert.equal(data.a[0].id, 'car'); + }); + + test('filter method deletes object from Array specified by special remove object', function() { + var data = { + a: [ + { + id: 'book', + price: 100 + }, + { + id: 'car', + price: 3456 + } + ], + b: { + c: 2, + d: 1 + } + }; + + jp.filter(data, '$..b.c', function(v) { + return true; + }); + + assert.equal(data.b.c, undefined); + assert.equal(data.b.d, 1); + }); + test('apply method applies survives structural changes', function() { var data = {a: {b: [1, {c: [2,3]}]}}; jp.apply(data, '$..*[?(@.length > 1)]', function(array) {