Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions benchmark.js
Original file line number Diff line number Diff line change
@@ -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')
137 changes: 57 additions & 80 deletions jsonpointer.js
Original file line number Diff line number Diff line change
@@ -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 === undefined) 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 undefined
}
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) {
Expand Down
15 changes: 15 additions & 0 deletions test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down