@hn3000/json-ref
Version:
simple resolver for json reference and json pointer
251 lines (213 loc) • 6.3 kB
text/typescript
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;
}