UNPKG

@hn3000/json-ref

Version:

simple resolver for json reference and json pointer

251 lines (213 loc) 6.3 kB
function isSimpleObj(val:any) { return ( (null == val) || (val instanceof Date) || (val instanceof String) || (val instanceof RegExp) ); } function maybeArrayIndex(val: string) { if ('-' == val) return true; let n = parseFloat(val); return !(isNaN(n) || Math.floor(n) !== n); } export class JsonPointer { /** * Returns a JsonPointer object from a string or JsonPointer. * Used to allow clients to pass in either a string or a JsonPointer without having * to deal with the difference. * * @param path string or JsonPointer */ static get(path: string|string[]|JsonPointer): JsonPointer { if (path instanceof JsonPointer) { return path; } return new JsonPointer(path); } /** * Collects all paths to objects deemed interesting by the predicated passed in. * * @param obj object to walk * @param pred predicate to decide whether an object's path should be returned */ public static paths(obj: any, pred?: (x:any, p: JsonPointer) => boolean): string[] { let result:string[] = []; if (typeof pred === 'function') { JsonPointer.walkObject(obj, (v, p) => (pred(v, p) && result.push(p.toString()), false)); } else { JsonPointer.walkObject(obj, (_, p) => (result.push(p.toString()), false)); } return result; } /** * Collects all pointers to objects deemed interesting by the predicated passed in. * * @param obj object to walk * @param pred predicate to decide whether an object's path should be returned */ public static pointers(obj: any, pred?: (x:any, p: JsonPointer) => boolean): JsonPointer[] { let result:JsonPointer[] = []; if (typeof pred === 'function') { JsonPointer.walkObject(obj, (v, p) => (pred(v, p) && result.push(p), false)); } else { JsonPointer.walkObject(obj, (_, p) => (result.push(p), false)); } return result; } /** * Walk obj and pass all values and their paths to the walker function. * * Stops descending into sub-objects if the walker returns true. */ public static walkObject(obj: any, walker: (val:any, p:JsonPointer) => boolean) { var queue: {val:any; path:JsonPointer;}[] = []; if (null != obj) { queue.push({ val: obj, path: new JsonPointer('')}); } while (0 != queue.length) { let { val, path } = queue.shift(); let keys = Object.keys(val); for (let k of keys) { let thisVal = val[k]; let thisPath = path.add(k); let more = !walker(thisVal, thisPath); let thisType = typeof(thisVal); if ((thisType === 'object') && more && !isSimpleObj(thisVal)) { //console.log(`descend into ${thisPath.asString()}`); queue.push({ val: thisVal, path: thisPath }); } } } } static deref(p:string, obj:any):any { return new JsonPointer(p).getValue(obj); } constructor(ref:string|string[]|JsonPointer, extraUnquoted?:string) { if (typeof ref === 'string') { this._keypath = (ref || "").split('/').slice(1).map(JsonPointer.unquote); } else if (Array.isArray(ref)) { this._keypath = ref.slice(); } else { this._keypath = ref._keypath.slice(); } if (null != extraUnquoted) { this._keypath.push(extraUnquoted); } } add(extraUnquoted:string):JsonPointer { return new JsonPointer(this, extraUnquoted); } hasParent(): boolean { return this._keypath.length != 0; } get parent():JsonPointer { let kp = this._keypath; let len = kp.length; if (len == 0) { return null; } return new JsonPointer(kp.slice(0, len-1)); } get(segment:number): string { let keypath = this._keypath; return keypath[segment + (segment < 0 ? keypath.length : 0)]; } public static unquote(s:string) { var result = s.replace(/~1/g, '/'); result = result.replace(/~0/g, '~'); return result; } public static quote(s:string) { var result = s.replace(/~/g, '~0'); result = result.replace(/\//g, '~1'); return result; } public static _deref(o:any, k:string) { let isDash = k == '-'; if (isDash) { let isArray = Array.isArray(o); if (isArray) { return o[o.length]; } } return o && o[k]; } public static _set(o:any, k:string, v: any) { let i: string|number = k; if ((k === '-') && Array.isArray(o)) { i = o.length; } o[i] = v; } getValue(obj:any):any { return this._keypath.reduce(JsonPointer._deref, obj); } setValue(obj:any, val:any, createPath: boolean = false): any { return this._changeValue(obj, val, createPath, useMutable); } withValue(obj:any, val:any, createPath: boolean = false): any { return this._changeValue(obj, val, createPath, copyImmutable); } _changeValue(obj:any, val:any, createPath: boolean, makeMutable: (x: any) => any): any { let keys = this._keypath; if (keys.length == 0) { return val; } let start = makeMutable(obj || (maybeArrayIndex(keys[0]) ? [] : {})); let tmp = start; let i = 0; for (let n = keys.length-1; i < n; ++i) { const k = keys[i]; const kval = JsonPointer._deref(tmp, k); let ntmp = kval; if (null == kval) { if (createPath || null == obj) { ntmp = maybeArrayIndex(keys[i+1]) ? [] : {} } else { tmp = null; break; } } else { ntmp = makeMutable(kval); } if (null != ntmp && kval !== ntmp) { JsonPointer._set(tmp, k, ntmp); } tmp = ntmp; } if (null != tmp) { JsonPointer._set(tmp, keys[i], val); } return start; } deleteValue(obj:any) { let last = this.get(-1); let parent = this.parent.getValue(obj); if (null != parent) { delete parent[last]; } } asString():string { return [''].concat(this._keypath.map(JsonPointer.quote)).join('/'); } toString():string { return this.asString(); } get keys():string[] { return this._keypath; } private _keypath:string[]; } function copyImmutable(ntmp: any) { let tmp; if (Array.isArray(ntmp)) { tmp = [ ... ntmp ]; } else { tmp = { ... ntmp }; } return tmp; } function useMutable(ntmp: any) { return ntmp; }