|
const hasExcape = /~/; |
|
const escapeMatcher = /~[01]/g; |
|
|
|
type Pointer = string | string[]; |
|
|
|
function escapeReplacer(m: string) { |
|
switch (m) { |
|
case "~1": |
|
return "/"; |
|
case "~0": |
|
return "~"; |
|
} |
|
throw new Error("Invalid tilde escape: " + m); |
|
} |
|
|
|
function untilde(str: string) { |
|
if (!hasExcape.test(str)) return str; |
|
return str.replace(escapeMatcher, escapeReplacer); |
|
} |
|
|
|
function setter(obj: any, pointer: Pointer, value: any) { |
|
let part: string | number; |
|
let hasNextPart: boolean; |
|
|
|
if (pointer[1] === "constructor" && pointer[2] === "prototype") return obj; |
|
if (pointer[1] === "__proto__") return obj; |
|
|
|
for (let 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] as any) < Infinity) || |
|
pointer[p] === "-" |
|
) |
|
obj[part] = []; |
|
else obj[part] = {}; |
|
} |
|
} |
|
|
|
if (!hasNextPart) break; |
|
obj = obj[part]; |
|
} |
|
|
|
const oldValue = obj[part!]; |
|
if (value === undefined) delete obj[part!]; |
|
else obj[part!] = value; |
|
return oldValue; |
|
} |
|
|
|
export function compilePointer(pointer: Pointer) { |
|
if (typeof pointer === "string") { |
|
pointer = pointer.split("/"); |
|
if (pointer[0] === "") return pointer; |
|
throw new Error("Invalid JSON pointer."); |
|
} else if (Array.isArray(pointer)) { |
|
return pointer; |
|
} |
|
|
|
throw new Error("Invalid JSON pointer."); |
|
} |
|
|
|
export function get(obj: any, pointer: Pointer) { |
|
if (typeof obj !== "object") throw new Error("Invalid input object."); |
|
pointer = compilePointer(pointer); |
|
const len = pointer.length; |
|
if (len === 1) return obj; |
|
|
|
for (let p = 1; p < len; ) { |
|
obj = obj[untilde(pointer[p++])]; |
|
if (len === p) return obj; |
|
if (typeof obj !== "object") return undefined; |
|
} |
|
} |
|
|
|
export function clone(obj: any, pointer: Pointer) { |
|
if (typeof obj !== "object") throw new Error("Invalid input object."); |
|
pointer = compilePointer(pointer); |
|
const len = pointer.length; |
|
if (len === 1) return obj; |
|
|
|
const rootPointer = Object.assign({}, obj); |
|
let levelPointer = rootPointer; |
|
|
|
for (let p = 1; p < len - 1; ) { |
|
const key = untilde(pointer[p++]); |
|
// Clone objects and arrays |
|
if (Array.isArray(levelPointer[key])) { |
|
levelPointer[key] = levelPointer[key].slice(); |
|
} else if ( |
|
levelPointer[key] !== null && |
|
typeof levelPointer[key] === "object" |
|
) { |
|
levelPointer[key] = Object.assign({}, levelPointer[key]); |
|
} |
|
levelPointer = levelPointer[key]; |
|
if (len === p) return rootPointer; |
|
} |
|
return rootPointer; |
|
} |
|
|
|
export function set(obj: any, pointer: Pointer, value: any) { |
|
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); |
|
} |
|
|
|
export function compile(pointer: Pointer) { |
|
const compiled = compilePointer(pointer); |
|
return { |
|
get: function (object: any) { |
|
return get(object, compiled); |
|
}, |
|
set: function (object: any, value: any) { |
|
return set(object, compiled, value); |
|
}, |
|
}; |
|
} |
Tom released this slightly updated version a few weeks later which has a generic: https://gist.github.com/tmcw/c5186205a22d74f9fc624ed07ef68716