From d4588b39c4d78b773a5cf15f2ce98bd7850f6e01 Mon Sep 17 00:00:00 2001 From: Kristian Mandrup Date: Mon, 31 Oct 2016 12:49:16 +0100 Subject: [PATCH 1/5] add special delete option to apply --- README.md | 4 +++- lib/index.js | 6 +++++- package.json | 10 +++++----- test/sugar.js | 6 ++++++ 4 files changed, 19 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 28585df..4743805 100755 --- a/README.md +++ b/README.md @@ -153,7 +153,7 @@ 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 as its only parameter. Returns matching nodes with their updated values. ```javascript @@ -166,6 +166,8 @@ var nodes = jp.apply(data, '$..author', function(value) { return value.toUpperCa // ] ``` +If the supplied application function returns the special value `:delete:` the node will be deleted and not updated as is otherwise the default case. + #### 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..1ed07f2 100755 --- a/lib/index.js +++ b/lib/index.js @@ -42,7 +42,11 @@ JSONPath.prototype.apply = function(obj, string, fn) { var key = node.path.pop(); var parent = this.value(obj, this.stringify(node.path)); var val = node.value = fn.call(obj, parent[key]); - parent[key] = val; + if (val === ':delete:') { + delete parent[key] + } else { + parent[key] = val; + } }, this); return nodes; diff --git a/package.json b/package.json index ed180f6..fad1ed6 100644 --- a/package.json +++ b/package.json @@ -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": "^3.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..8469eee 100644 --- a/test/sugar.js +++ b/test/sugar.js @@ -17,6 +17,12 @@ suite('sugar', function() { assert.equal(data.z.a, 101); }); + test('apply method deletes value', function() { + var data = { a: 1, b: 2, c: 3, z: { a: 100, b: 200 } }; + jp.apply(data, '$..a', function(v) { return ':delete:' }); + assert.ok(Object.keys(data).indexOf('a') < 0); + }); + test('apply method applies survives structural changes', function() { var data = {a: {b: [1, {c: [2,3]}]}}; jp.apply(data, '$..*[?(@.length > 1)]', function(array) { From cb51ed0bd1304ffe607c5a6e6fb85f571b6d7615 Mon Sep 17 00:00:00 2001 From: Kristian Mandrup Date: Mon, 31 Oct 2016 13:01:17 +0100 Subject: [PATCH 2/5] downgrade to esprima v2. v3 raises Unexpected token ILLEGAL --- lib/index.js | 3 ++- package.json | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/index.js b/lib/index.js index 1ed07f2..81df23c 100755 --- a/lib/index.js +++ b/lib/index.js @@ -42,8 +42,9 @@ JSONPath.prototype.apply = function(obj, string, fn) { var key = node.path.pop(); var parent = this.value(obj, this.stringify(node.path)); var val = node.value = fn.call(obj, parent[key]); + if (val === ':delete:') { - delete parent[key] + delete parent[key]; } else { parent[key] = val; } diff --git a/package.json b/package.json index fad1ed6..2465249 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "generate": "node bin/generate_parser.js > generated/parser.js" }, "dependencies": { - "esprima": "^3.0.0", + "esprima": "^2.0.0", "jison": "^0.4.17", "static-eval": "^1.1.1", "underscore": "^1.8.3" From 06e9a1d8c6163321f0293a0243bcbad861bae6fe Mon Sep 17 00:00:00 2001 From: Kristian Mandrup Date: Mon, 31 Oct 2016 13:39:15 +0100 Subject: [PATCH 3/5] enable delete from parent Array or Object using special return value on apply --- lib/index.js | 38 +++++++++++++++++++++++++++++++++--- package.json | 4 +++- test/sugar.js | 53 +++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 91 insertions(+), 4 deletions(-) diff --git a/lib/index.js b/lib/index.js index 81df23c..9d15696 100755 --- a/lib/index.js +++ b/lib/index.js @@ -2,6 +2,7 @@ var assert = require('assert'); var dict = require('./dict'); var Parser = require('./parser'); var Handlers = require('./handlers'); +var remove = require('lodash.remove'); var JSONPath = function() { this.initialize.apply(this, arguments); @@ -27,6 +28,10 @@ JSONPath.prototype.parent = function(obj, string) { return this.value(obj, node.path); } +function isObject(obj) { + return obj === Object(obj); +} + JSONPath.prototype.apply = function(obj, string, fn) { assert.ok(obj instanceof Object, "obj needs to be an object"); @@ -38,15 +43,42 @@ JSONPath.prototype.apply = function(obj, string, fn) { return b.path.length - a.path.length; }); + function removeFromParent(parent, val) { + if (!isObject(val)) return; + if (!val.remove) return; + + if (typeof val.remove !== 'function') { + if (!isObject(val.remove)) return; + + // key: 'id' + // match: v.id + + var key = val.remove.key; + var match = val.remove.match; + + val.remove = function(obj) { + return obj[key] === match; + } + } + + // use special remove function to remove this node from parent Array + remove(parent, val.remove) + } + 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]); - if (val === ':delete:') { - delete parent[key]; + if (Array.isArray(parent)) { + removeFromParent(parent, val) } else { - parent[key] = val; + // parent must be an Object + if (val === ':delete:') { + delete parent[key]; + } else { + parent[key] = val; + } } }, this); diff --git a/package.json b/package.json index 2465249..05abd1a 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", @@ -11,6 +11,8 @@ "dependencies": { "esprima": "^2.0.0", "jison": "^0.4.17", + "lodash.filter": "^4.6.0", + "lodash.remove": "^4.7.0", "static-eval": "^1.1.1", "underscore": "^1.8.3" }, diff --git a/test/sugar.js b/test/sugar.js index 8469eee..b7dda5e 100644 --- a/test/sugar.js +++ b/test/sugar.js @@ -23,6 +23,59 @@ suite('sugar', function() { assert.ok(Object.keys(data).indexOf('a') < 0); }); + test('apply method deletes object from Array specified by remove function', function() { + var data = { + a: [ + { + id: 'book', + price: 100 + }, + { + id: 'car', + price: 3456 + } + ], + b: 2 + }; + + jp.apply(data, '$..a[0]', function(v) { + return { + remove: (obj) => { + return obj.id === v.id; + } + } + }); + + assert.equal(data.a[0].id, 'car'); + }); + + test('apply method deletes object from Array specified by special remove object', function() { + var data = { + a: [ + { + id: 'book', + price: 100 + }, + { + id: 'car', + price: 3456 + } + ], + b: 2 + }; + + jp.apply(data, '$..a[0]', function(v) { + return { + remove: { + key: 'id', + match: v.id + } + } + }); + + assert.equal(data.a[0].id, 'car'); + }); + test('apply method applies survives structural changes', function() { var data = {a: {b: [1, {c: [2,3]}]}}; jp.apply(data, '$..*[?(@.length > 1)]', function(array) { From fd7aac51bdf2fd1efdfdfd9f02e8fd102d2c4d46 Mon Sep 17 00:00:00 2001 From: Kristian Mandrup Date: Mon, 31 Oct 2016 14:06:04 +0100 Subject: [PATCH 4/5] change remove key to sth more unique and update docs with delete examples --- README.md | 43 +++++++++++++++++++++++++++++++++++++++++++ lib/index.js | 14 +++++++------- test/sugar.js | 4 ++-- 3 files changed, 52 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 4743805..5c161cf 100755 --- a/README.md +++ b/README.md @@ -168,6 +168,49 @@ var nodes = jp.apply(data, '$..author', function(value) { return value.toUpperCa If the supplied application function returns the special value `:delete:` the node will be deleted and not updated as is otherwise the default case. +When deleting from an Array parent node, you need to pass a special remove object which contains information for the parent node to figure out which child to remove. + +```js +var data = { + a: [ + { + id: 'book', + price: 100 + }, + { + id: 'car', + price: 3456 + } + ], + b: 2 +}; +``` + +Remove the first element in `a` + +```js +jp.apply(data, '$..a[0]', function(v) { + return { + removeItem: (obj) => { + return obj.id === v.id; + } + } +}); +``` + +Use special shorthand object to remove by identity key: + +```js +jp.apply(data, '$..a[0]', function(v) { + return { + removeItem: { + key: 'id', + match: v.id + } + } +}); +``` + #### 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 9d15696..ad14a0a 100755 --- a/lib/index.js +++ b/lib/index.js @@ -45,24 +45,24 @@ JSONPath.prototype.apply = function(obj, string, fn) { function removeFromParent(parent, val) { if (!isObject(val)) return; - if (!val.remove) return; + if (!val.removeItem) return; - if (typeof val.remove !== 'function') { - if (!isObject(val.remove)) return; + if (typeof val.removeItem !== 'function') { + if (!isObject(val.removeItem)) return; // key: 'id' // match: v.id - var key = val.remove.key; - var match = val.remove.match; + var key = val.removeItem.key; + var match = val.removeItem.match; - val.remove = function(obj) { + val.removeItem = function(obj) { return obj[key] === match; } } // use special remove function to remove this node from parent Array - remove(parent, val.remove) + remove(parent, val.removeItem) } nodes.forEach(function(node) { diff --git a/test/sugar.js b/test/sugar.js index b7dda5e..16daa69 100644 --- a/test/sugar.js +++ b/test/sugar.js @@ -40,7 +40,7 @@ suite('sugar', function() { jp.apply(data, '$..a[0]', function(v) { return { - remove: (obj) => { + removeItem: (obj) => { return obj.id === v.id; } } @@ -66,7 +66,7 @@ suite('sugar', function() { jp.apply(data, '$..a[0]', function(v) { return { - remove: { + removeItem: { key: 'id', match: v.id } From f55ac4c2528a0153caf45db289e4984313d01ed9 Mon Sep 17 00:00:00 2001 From: Kristian Mandrup Date: Tue, 1 Nov 2016 11:45:42 +0100 Subject: [PATCH 5/5] added filter method --- Changelog.md | 6 ++++++ README.md | 50 ++++++++++++++++++++++-------------------- lib/index.js | 60 +++++++++++++++++++++------------------------------ package.json | 2 -- test/sugar.js | 35 +++++++++++------------------- 5 files changed, 70 insertions(+), 83 deletions(-) create mode 100644 Changelog.md 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 5c161cf..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 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 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,9 +166,11 @@ var nodes = jp.apply(data, '$..author', function(value) { return value.toUpperCa // ] ``` -If the supplied application function returns the special value `:delete:` the node will be deleted and not updated as is otherwise the default case. +#### jp.filter(obj, pathExpression, fn) -When deleting from an Array parent node, you need to pass a special remove object which contains information for the parent node to figure out which child to remove. +Filters the data by removing the matched value if the callback returns a truthy value. + +Sample data: ```js var data = { @@ -182,33 +184,35 @@ var data = { price: 3456 } ], - b: 2 + b: { + c: 2, + d: 1 + } }; ``` -Remove the first element in `a` +Example: Using `filter` to remove element from an `Array` or `Object` parent node. ```js -jp.apply(data, '$..a[0]', function(v) { - return { - removeItem: (obj) => { - return obj.id === v.id; - } - } +// Remove the first element in `a` +jp.filter(data, '$..a[0]', function(v, ctx) { + return true; }); -``` -Use special shorthand object to remove by identity key: - -```js -jp.apply(data, '$..a[0]', function(v) { - return { - removeItem: { - key: 'id', - match: v.id - } - } +// 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) diff --git a/lib/index.js b/lib/index.js index ad14a0a..a0c7246 100755 --- a/lib/index.js +++ b/lib/index.js @@ -2,7 +2,6 @@ var assert = require('assert'); var dict = require('./dict'); var Parser = require('./parser'); var Handlers = require('./handlers'); -var remove = require('lodash.remove'); var JSONPath = function() { this.initialize.apply(this, arguments); @@ -28,12 +27,7 @@ JSONPath.prototype.parent = function(obj, string) { return this.value(obj, node.path); } -function isObject(obj) { - return obj === Object(obj); -} - -JSONPath.prototype.apply = function(obj, string, fn) { - +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") @@ -43,43 +37,39 @@ JSONPath.prototype.apply = function(obj, string, fn) { return b.path.length - a.path.length; }); - function removeFromParent(parent, val) { - if (!isObject(val)) return; - if (!val.removeItem) return; + 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 (typeof val.removeItem !== 'function') { - if (!isObject(val.removeItem)) return; + if (!val) return; - // key: 'id' - // match: v.id + if (Array.isArray(parent)) { + parent.splice(key, 1); + return; + } + delete parent[key]; + }, this); - var key = val.removeItem.key; - var match = val.removeItem.match; + return nodes; +} - val.removeItem = function(obj) { - return obj[key] === match; - } - } +JSONPath.prototype.apply = function(obj, string, fn) { - // use special remove function to remove this node from parent Array - remove(parent, val.removeItem) - } + 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]); - - if (Array.isArray(parent)) { - removeFromParent(parent, val) - } else { - // parent must be an Object - if (val === ':delete:') { - delete parent[key]; - } else { - parent[key] = val; - } - } + var val = node.value = fn.call(obj, parent[key], {parent: parent, key: key}); + parent[key] = val; }, this); return nodes; diff --git a/package.json b/package.json index 05abd1a..4f12f2a 100644 --- a/package.json +++ b/package.json @@ -11,8 +11,6 @@ "dependencies": { "esprima": "^2.0.0", "jison": "^0.4.17", - "lodash.filter": "^4.6.0", - "lodash.remove": "^4.7.0", "static-eval": "^1.1.1", "underscore": "^1.8.3" }, diff --git a/test/sugar.js b/test/sugar.js index 16daa69..ca07b43 100644 --- a/test/sugar.js +++ b/test/sugar.js @@ -17,13 +17,7 @@ suite('sugar', function() { assert.equal(data.z.a, 101); }); - test('apply method deletes value', function() { - var data = { a: 1, b: 2, c: 3, z: { a: 100, b: 200 } }; - jp.apply(data, '$..a', function(v) { return ':delete:' }); - assert.ok(Object.keys(data).indexOf('a') < 0); - }); - - test('apply method deletes object from Array specified by remove function', function() { + test('filter method deletes object from Array specified by remove function', function() { var data = { a: [ { @@ -38,18 +32,14 @@ suite('sugar', function() { b: 2 }; - jp.apply(data, '$..a[0]', function(v) { - return { - removeItem: (obj) => { - return obj.id === v.id; - } - } + jp.filter(data, '$..a[0]', function(v) { + return true; }); assert.equal(data.a[0].id, 'car'); }); - test('apply method deletes object from Array specified by special remove object', function() { + test('filter method deletes object from Array specified by special remove object', function() { var data = { a: [ { @@ -61,19 +51,18 @@ suite('sugar', function() { price: 3456 } ], - b: 2 + b: { + c: 2, + d: 1 + } }; - jp.apply(data, '$..a[0]', function(v) { - return { - removeItem: { - key: 'id', - match: v.id - } - } + jp.filter(data, '$..b.c', function(v) { + return true; }); - assert.equal(data.a[0].id, 'car'); + assert.equal(data.b.c, undefined); + assert.equal(data.b.d, 1); }); test('apply method applies survives structural changes', function() {