From 54b36b956217b2b6e1986d658094267c300eaa40 Mon Sep 17 00:00:00 2001 From: Marc Bachmann Date: Thu, 3 Dec 2015 00:34:51 +0100 Subject: [PATCH 1/3] Improve performance MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Split up getter and setter. - Don’t modify pointers, use faster array operations. - Only apply untilde when necessary. --- jsonpointer.js | 137 ++++++++++++++++++++----------------------------- 1 file changed, 57 insertions(+), 80 deletions(-) diff --git a/jsonpointer.js b/jsonpointer.js index a7b3338..e1f080e 100644 --- a/jsonpointer.js +++ b/jsonpointer.js @@ -1,105 +1,82 @@ -var untilde = function (str) { - return str.replace(/~./g, function (m) { - switch (m) { - case '~0': return '~' - case '~1': return '/' - } - throw new Error('Invalid tilde escape: ' + m) - }) +var hasExcape = /~/ +var escapeMatcher = /~[01]/g +function escapeReplacer (m) { + switch (m) { + case '~1': return '/' + case '~0': return '~' + } + throw new Error('Invalid tilde escape: ' + m) } -var traverse = function (obj, pointer, value) { - var part = untilde(pointer.shift()) - var isJustReading = arguments.length === 2 - - if (obj[part] == null) { - if (isJustReading) return null - - // support setting of /- - if (part === '-' && obj instanceof Array) { - part = obj.length - } - - // support nested objects/array when setting values - var nextPart = pointer[0] - if (nextPart === '-' || !isNaN(nextPart)) { - obj[part] = [] - } else if (nextPart) { - obj[part] = {} - } - } +function untilde (str) { + if (!hasExcape.test(str)) return str + return str.replace(escapeMatcher, escapeReplacer) +} - // keep traversing - if (pointer.length !== 0) { - if (isJustReading) { - return traverse(obj[part], pointer) - } else { - return traverse(obj[part], pointer, value) +function setter (obj, pointer, value) { + var part + var hasNextPart + + for (var p = 1, len = pointer.length; p < len;) { + part = untilde(pointer[p++]) + hasNextPart = len > p + + if (typeof obj[part] === 'undefined') { + // support setting of /- + if (Array.isArray(obj) && part === '-') { + part = obj.length + } + + // support nested objects/array when setting values + if (hasNextPart) { + if ((pointer[p] !== '' && pointer[p] < Infinity) || pointer[p] === '-') obj[part] = [] + else obj[part] = {} + } } - } - // we're done - if (isJustReading) { - return obj[part] + if (!hasNextPart) break + obj = obj[part] } - // set new value, return old value var oldValue = obj[part] - if (value === null) { - delete obj[part] - } else { - obj[part] = value - } + if (value === null) delete obj[part] + else obj[part] = value return oldValue } -var compilePointer = function (pointer) { - if (pointer === '') { - return [] - } - - if (!pointer) { - throw new Error('Invalid JSON pointer.') - } - - if (!(pointer instanceof Array)) { +function compilePointer (pointer) { + if (typeof pointer === 'string') { pointer = pointer.split('/') - if (pointer.shift() !== '') throw new Error('Invalid JSON pointer.') - } else { - // Clone the pointer array - var newPointer = [] - for (var i = 0; i < pointer.length; i++) newPointer[i] = pointer[i] - pointer = newPointer + if (pointer[0] === '') return pointer + throw new Error('Invalid JSON pointer.') + } else if (Array.isArray(pointer)) { + return pointer } - return pointer + throw new Error('Invalid JSON pointer.') } -var validateInput = function (obj, pointer) { - if (typeof obj !== 'object') { - throw new Error('Invalid input object.') - } +function get (obj, pointer) { + if (typeof obj !== 'object') throw new Error('Invalid input object.') + pointer = compilePointer(pointer) + var len = pointer.length + if (len === 1) return obj - return compilePointer(pointer) -} - -var get = function (obj, pointer) { - pointer = validateInput(obj, pointer) - if (pointer.length === 0) { - return obj + for (var p = 1; p < len;) { + obj = obj[untilde(pointer[p++])] + if (len === p) return obj + if (typeof obj !== 'object') return null } - return traverse(obj, pointer) } -var set = function (obj, pointer, value) { - pointer = validateInput(obj, pointer) - if (pointer.length === 0) { - throw new Error('Invalid JSON pointer for set.') - } - return traverse(obj, pointer, value) +function set (obj, pointer, value) { + if (typeof obj !== 'object') throw new Error('Invalid input object.') + pointer = compilePointer(pointer) + if (pointer.length === 0) throw new Error('Invalid JSON pointer for set.') + return setter(obj, pointer, value) } -var compile = function (pointer) { +function compile (pointer) { var compiled = compilePointer(pointer) return { get: function (object) { From f5de538822ea2f6c3fe5210b9a5a869da3f62e2d Mon Sep 17 00:00:00 2001 From: Marc Bachmann Date: Wed, 11 May 2016 11:36:59 +0100 Subject: [PATCH 2/3] Add simple benchmark --- benchmark.js | 56 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 benchmark.js diff --git a/benchmark.js b/benchmark.js new file mode 100644 index 0000000..8a95636 --- /dev/null +++ b/benchmark.js @@ -0,0 +1,56 @@ +var jsonpointer = require('./') + +var i +var obj = { + a: 1, + b: { + c: 2 + }, + d: { + e: [{ a: 3 }, { b: 4 }, { c: 5 }] + } +} + +// Get +console.time('get first level property') +for (i = 0; i < 1e6; i++) { + jsonpointer.get(obj, '/a') +} +console.timeEnd('get first level property') + +console.time('get second level property') +for (i = 0; i < 1e6; i++) { + jsonpointer.get(obj, '/d/e') +} +console.timeEnd('get second level property') + +console.time('get third level property') +for (i = 0; i < 1e6; i++) { + jsonpointer.get(obj, '/d/e/0') +} +console.timeEnd('get third level property') + +// Set +console.time('set first level property') +for (i = 0; i < 1e6; i++) { + jsonpointer.set(obj, '/a', 'bla') +} +console.timeEnd('set first level property') + +console.time('set second level property') +for (i = 0; i < 1e6; i++) { + jsonpointer.set(obj, '/d/e', 'bla') +} +console.timeEnd('set second level property') + +console.time('set third level property') +for (i = 0; i < 1e6; i++) { + jsonpointer.set(obj, '/d/e/0', 'bla') +} +console.timeEnd('set third level property') + +console.time('push property into array') +for (i = 0; i < 1e6; i++) { + jsonpointer.set(obj, '/d/e/-', 'bla') +} +console.timeEnd('push property into array') From baa6607a5b853768b456c4558482087e4faba84c Mon Sep 17 00:00:00 2001 From: Matt Harrison Date: Wed, 18 May 2016 20:46:20 +0100 Subject: [PATCH 3/3] Support null as a valid value, unset via undefined --- jsonpointer.js | 4 ++-- test.js | 15 +++++++++++++++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/jsonpointer.js b/jsonpointer.js index e1f080e..7cfaec0 100644 --- a/jsonpointer.js +++ b/jsonpointer.js @@ -39,7 +39,7 @@ function setter (obj, pointer, value) { } var oldValue = obj[part] - if (value === null) delete obj[part] + if (value === undefined) delete obj[part] else obj[part] = value return oldValue } @@ -65,7 +65,7 @@ function get (obj, pointer) { for (var p = 1; p < len;) { obj = obj[untilde(pointer[p++])] if (len === p) return obj - if (typeof obj !== 'object') return null + if (typeof obj !== 'object') return undefined } } diff --git a/test.js b/test.js index 8e48b46..e3d9963 100644 --- a/test.js +++ b/test.js @@ -40,10 +40,25 @@ assert.equal(jsonpointer.get(obj, '/d/e/0/a'), 4) assert.equal(jsonpointer.get(obj, '/d/e/1/b'), 5) assert.equal(jsonpointer.get(obj, '/d/e/2/c'), 6) +// can set `null` as a value +assert.equal(jsonpointer.set(obj, '/f/g/h/foo/0', null), 'test') +assert.strictEqual(jsonpointer.get(obj, '/f/g/h/foo/0'), null) +assert.equal(jsonpointer.set(obj, '/b/c', null), 3) +assert.strictEqual(jsonpointer.get(obj, '/b/c'), null) + assert.equal(jsonpointer.get(obj, ''), obj) assert.throws(function () { jsonpointer.get(obj, 'a') }, validateError) assert.throws(function () { jsonpointer.get(obj, 'a/') }, validateError) +// can unset values with `undefined` +jsonpointer.set(obj, '/a', undefined) +assert.strictEqual(jsonpointer.get(obj, '/a'), undefined) +jsonpointer.set(obj, '/d/e/1', undefined) +assert.strictEqual(jsonpointer.get(obj, '/d/e/1'), undefined) + +// returns `undefined` when path extends beyond any existing objects +assert.strictEqual(jsonpointer.get(obj, '/x/y/z'), undefined) + function validateError (err) { if ((err instanceof Error) && /Invalid JSON pointer/.test(err.message)) { return true