UNPKG

pointer-props

Version:

JavaScript object manipulation (get/set/del) using JSON Pointer (RFC6901) and JSON Reference paths.

118 lines (104 loc) 3.33 kB
import { dset } from 'dset' import dlv from 'dlv' class InfiniteReference extends Error { constructor(props) { super(props) this.name = 'InfiniteReference' } } class ExternalReference extends Error { constructor(props) { super(props) this.name = 'ExternalReference' } } /* Note on escaping order, from RFC6901: > Evaluation of each reference token begins by decoding any escaped > character sequence. This is performed by first transforming any > occurrence of the sequence '~1' to '/', and then transforming any > occurrence of the sequence '~0' to '~'. By performing the > substitutions in this order, an implementation avoids the error of > turning '~01' first into '~1' and then into '/', which would be > incorrect (the string '~01' correctly becomes '~1' after > transformation). */ /** * Convert a JSON Pointer into a list of unescaped tokens, e.g. `/foo/bar~1biz` to `['foo','bar/biz']`. * @type {import("../index").toTokens} */ export const toTokens = function (path) { if (!path.startsWith('/') && !path.startsWith('#/')) throw new ExternalReference(`Non-relative JSON Pointers are not supported: ${path}`) ;[ , ...path ] = path.split('/') let segments = [] for (let segment of path) { segments.push(segment.replaceAll('~1', '/').replaceAll('~0', '~')) } return segments } /** * Convert a list of unescaped tokens to a JSON Pointer, e.g. `['foo','bar/biz']` to `/foo/bar~1biz`. * @type {import("../index").toPointer} */ export const toPointer = function (list) { let output = '' for (let segment of list) { output += '/' + segment.toString().replaceAll('~', '~0').replaceAll('/', '~1') } return output } /** * @param {String|Array<String>} input * @returns {Array<String>} */ const makeConsistent = input => input.split ? toTokens(input) : input /** * Access a property by JSON Pointer, or by an array of property tokens. * @type {import("../index").get} */ export const get = function (obj, path) { return dlv(obj, makeConsistent(path)) } /** * Set a deep property by JSON Pointer, or by an array of property tokens. * @type {import("../index").set} */ export function set(obj, path, value) { dset(obj, makeConsistent(path), value) return obj } /** * Remove a deep property by JSON Pointer, or by an array of property tokens. * @type {import("../index").del} */ export function del(obj, path) { let segments = makeConsistent(path) let last = segments.pop() let item = dlv(obj, segments) if (Array.isArray(item)) item.splice(parseInt(last, 10), 1) else if (item) delete item[last] dset(obj, segments, item) return obj } /** * Resolve JSON Reference links to the final absolute array of accessor tokens. * @type {import("../index").resolve} */ export function resolve(obj, path) { const traversed = Object.create(null) const _resolve = keys => { let tempKeys = [ ...keys ] let tempObj = obj for (let index = 0; index < keys.length; index++) { if (tempObj === undefined) return null tempObj = tempObj[tempKeys.shift()] if (tempObj && tempObj.$ref) { if (traversed[tempObj.$ref]) throw new InfiniteReference(`Found a cycle of $ref names on: ${tempObj.$ref}`) traversed[tempObj.$ref] = true return _resolve([ ...toTokens(tempObj.$ref), ...tempKeys ]) } } return keys } return _resolve(makeConsistent(path)) }