Skip to content

Instantly share code, notes, and snippets.

@dschnare
Last active September 13, 2019 18:43
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save dschnare/28d5e8930a906ef82f0fafaefcb2a848 to your computer and use it in GitHub Desktop.
Save dschnare/28d5e8930a906ef82f0fafaefcb2a848 to your computer and use it in GitHub Desktop.
JSONPointer and walkObject APIs
const walkObject = (function () {
function walkObjectRec (obj, visit, { pointer = '', key = '', visited = new WeakMap(), parent = undefined } = {}) {
if (obj === null || obj === undefined) {
visit(obj, key, pointer, parent)
} else if (typeof obj === 'object') {
if (visited.has(obj)) {
visited.set(obj, visited.get(obj) + 1)
} else {
visited.set(obj, 1)
visit(obj, key, pointer, parent)
Object.keys(obj).forEach(key => {
walkObjectRec(obj[key], visit, {
key,
visited,
parent: obj,
pointer: JSONPointer.append(pointer, key)
})
})
}
} else {
visit(obj, key, pointer, parent)
}
}
return (obj, visit, { visited = new WeakMap() } = {}) => {
return walkObjectRec(obj, visit, { visited })
}
}())
const JSONPointer = {
isJSONPointer (pointer) {
// // Is a relative pointer? Then we consume the leading digits
// if (typeof pointer === 'string') {
// let p = 0
// let digits = ''
// for (p = 0; pointer[p] >= '0' && pointer[0] <= '9'; p += 1) {
// digits += pointer[p]
// }
// if (digits.length) {
// // Relative JSON pointers cannot start with '0'
// if (digits.length > 1 && digits[0] === '0') return false
// pointer = pointer.slice(p)
// if (pointer === '#') return true
// // Relative JSON pointers cannot be used in URI fragments so the
// // trailing JSON pointer cannot start with '#'
// if (pointer.indexOf('#') >= 0) return false
// }
// }
let valid = typeof pointer === 'string' &&
(
pointer === '' ||
pointer === '#' ||
pointer.startsWith('/') ||
pointer.startsWith('#/')
)
pointer = valid && pointer.startsWith('#')
? decodeURI(pointer).slice(1)
: pointer
if (valid && pointer !== '') {
let len = pointer.length
let c = ''
for (let i = 0; i < len && valid; i += 1) {
c = pointer[i]
if (c === '/') {
valid = true
} else if (c === '~') {
valid = pointer[i + 1] === '0' || pointer[i + 1] === '1'
} else {
valid = (c >= '\u0000' && c <= '\u002E') ||
(c >= '\u0030' && c <= '\u007D') ||
(c >= '\u007F' && c <= '\u10FFFF')
}
}
}
return valid
},
// isRelativeJSONPointer (pointer) {
// if (!this.isJSONPointer(pointer)) return false
// return pointer[0] >= '0' && pointer[0] <= '9'
// },
evaluate (pointer, document, { insertMissing = false } = {}) {
if (!this.isJSONPointer(pointer)) {
throw new Error(`Invalid pointer syntax. (${pointer})`)
}
if (!document || typeof document !== 'object') {
throw new Error('Invalid document')
}
// if (this.isRelativeJSONPointer(pointer)) {
// if (typeof reference !== 'string') {
// throw new Error('Argument "reference" must be a string')
// }
// let p = 0
// let digits = ''
// for (p = 0; pointer[p] >= '0' && pointer[0] <= '9'; p += 1) {
// digits += pointer[p]
// }
// const steps = parseInt(digits, 10)
// pointer = pointer.slice(p)
// reference = reference.startsWith('#')
// ? decodeURI(reference).slice(1)
// : reference
// const refSegments = reference.split('/')
// const newRefSegments = refSegments.slice(0, refSegments.length - steps)
// if (refSegments.length < steps) {
// // FAIL
// }
// if (pointer === '#') {
// return newRefSegments.pop()
// } else if (pointer.startsWith('/')) {
// pointer = newRefSegments.join('/')
// }
// }
const referenceTokens = (
pointer.startsWith('#')
? decodeURI(pointer).slice(1)
: pointer
).split('/')
let len = referenceTokens.length
let v = document
let key = ''
if (len === 1) return document
for (let k = 1; k < len; k += 1) {
key = referenceTokens[k]
if (key.includes('~')) {
key = key.replace(/~1/g, '/').replace(/~0/g, '~')
}
if (v === null || v === undefined) {
throw new Error(`JSON pointer references a nonexistent value. (${pointer})`)
}
if (Array.isArray(v) && key.match(/^\D$/)) {
throw new Error(`JSON pointer references a nonexistent array index. (${pointer})`)
}
if (typeof v === 'object') {
if (key in v) {
v = v[key]
} else if (insertMissing) {
v = v[key] = {}
} else {
throw new Error(`JSON pointer references a nonexistent value. (${pointer})`)
}
} else {
throw new Error(`JSON pointer references a nonexistent value. (${pointer})`)
}
}
return v
},
append (pointer, ...keys) {
if (!this.isJSONPointer(pointer)) {
throw new Error(`Invalid pointer syntax. (${pointer})`)
}
const isFrag = pointer.startsWith('#')
keys.forEach(key => {
if (typeof key !== 'string') {
throw new Error(`Invalid key. (${key})`)
}
key = key.replace(/~/g, '~0').replace(/\//g, '~1')
if (isFrag) key = encodeURIComponent(key)
pointer = `${pointer}/${key}`
if (!this.isJSONPointer(pointer)) {
throw new Error(`Key cannot be converted to reference token. (${key})`)
}
})
return pointer
},
get (pointer, document, { insertMissing = false } = {}) {
return this.evaluate(pointer, document, { insertMissing })
},
set (pointer, document, value, { insertMissing = false } = {}) {
if (!this.isJSONPointer(pointer)) {
throw new Error(`Invalid pointer syntax. (${pointer})`)
}
const keys = (
pointer.startsWith('#')
? decodeURI(pointer).slice(1)
: pointer
).split('/')
if (!keys.length) {
throw new Error('Empty JSON pointer cannot be set')
}
const obj = this.get(keys.slice(0, -1).join('/'), document, { insertMissing })
if (obj && typeof obj === 'object') {
const key = keys.pop()
if (key.includes('~')) {
key = key.replace(/~1/g, '/').replace(/~0/g, '~')
}
obj[key] = value
} else {
throw new Error(`JSON pointer references a nonexistent value. (${pointer})`)
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment