Skip to content

Commit

Permalink
implemented relative pointers
Browse files Browse the repository at this point in the history
  • Loading branch information
Phillip Clark committed May 13, 2021
1 parent bdd6317 commit 2485c9e
Show file tree
Hide file tree
Showing 5 changed files with 122 additions and 3 deletions.
52 changes: 52 additions & 0 deletions __tests__/pointer.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -474,4 +474,56 @@ describe('JsonPointer', () => {
expect(p.uriFragmentIdentifier).to.eql('#/foo');
});
});

describe('.relative method', () => {
const doc = {
foo: ['bar', 'baz'],
highly: {
nested: {
objects: true,
},
},
};

it('throws when relative pointer unspecified', () => {
const p = new JsonPointer('/highly/nested/objects');
expect(() => p.relative(doc, undefined)).to.throw(
'Invalid type: Relative JSON Pointers are represented as strings.',
);
});
it('throws when relative pointer empty', () => {
const p = new JsonPointer('/highly/nested/objects');
expect(() => p.relative(doc, '')).to.throw(
'Invalid Relative JSON Pointer syntax. Relative pointer must begin with a non-negative integer, followed by either the number sign (#), or a JSON Pointer.',
);
});
it('throws when relative pointer invalid [0](NaN)', () => {
const p = new JsonPointer('/highly/nested/objects');
expect(() => p.relative(doc, 'b/z')).to.throw(
'Invalid Relative JSON Pointer syntax. Relative pointer must begin with a non-negative integer, followed by either the number sign (#), or a JSON Pointer.',
);
});
it('throws when relative pointer invalid 1#/z', () => {
const p = new JsonPointer('/highly/nested/objects');
expect(() => p.relative(doc, '1#/z')).to.throw(
'Invalid Relative JSON Pointer syntax. Relative pointer must begin with a non-negative integer, followed by either the number sign (#), or a JSON Pointer.',
);
});
it('Spec examples 1', () => {
const p = new JsonPointer('/foo/1');
expect(p.relative(doc, '0')).to.eql('baz');
expect(p.relative(doc, '1/0')).to.eql('bar');
expect(p.relative(doc, '2/highly/nested/objects')).to.eql(true);
expect(p.relative(doc, '0#')).to.eql(1);
expect(p.relative(doc, '1#')).to.eql('foo');
});
it('Spec examples 2', () => {
const p = new JsonPointer('/highly/nested');
expect(p.relative(doc, '0/objects')).to.eql(true);
expect(p.relative(doc, '1/nested/objects')).to.eql(true);
expect(p.relative(doc, '2/foo/0')).to.eql('bar');
expect(p.relative(doc, '0#')).to.eql('nested');
expect(p.relative(doc, '1#')).to.eql('highly');
});
});
});
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
"build:watch": "tsc -w --importHelpers -p tsconfig.release.json",
"lint": "eslint . --ext .ts,.tsx",
"lint:fix": "eslint . --ext .ts,.tsx --fix",
"pretest": "npm run lint",
"pretest": "npm run lint:fix",
"test": "nyc mocha __tests__/**/*.spec.ts",
"test:watch": "chokidar \"*.js\" \"*.json\" \"src/**/*.ts\" \"__tests__/**/*.ts\" --command \"npm run test\" --initial",
"cilint": "eslint . --ext .ts,.tsx --format junit --output-file ./reports/eslint/eslint.xml",
Expand Down Expand Up @@ -91,4 +91,4 @@
"dependencies": {
"tslib": "^2.2.0"
}
}
}
33 changes: 33 additions & 0 deletions src/pointer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
encodeUriFragmentIdentifier,
pickDecoder,
unsetValueAtPath,
decodeRelativePointer,
} from './util';
import {
JsonStringPointer,
Expand Down Expand Up @@ -535,6 +536,38 @@ export class JsonPointer {
return typeof this.get(target) !== 'undefined';
}

/**
* Gets the value in the object graph that is the parent of the pointer location.
* @param target the target of the operation
*/
parent(target: unknown): unknown {
const p = this.path;
if (p.length == 1) return undefined;
const parent = new JsonPointer(p.slice(0, p.length - 1));
return parent.get(target);
}

/**
* Resolves the specified relative pointer path against the specified target object, and gets the target object's value at the relative pointer's location.
* @param target the target of the operation
* @param rel the relative pointer (relative to this)
* @returns the value at the relative pointer's resolved path; otherwise undefined.
*/
relative(target: unknown, rel: JsonStringPointer): unknown {
const p = this.path;
const decoded = decodeRelativePointer(rel) as string[];
const n = parseInt(decoded[0]);
const r = p.slice(0, p.length - n).concat(decoded.slice(1));
const other = new JsonPointer(r);
if (decoded[0][decoded[0].length - 1] == '#') {
// It references the path segment/name, not the value
const name = r[r.length - 1] as string;
const parent = other.parent(target);
return Array.isArray(parent) ? parseInt(name, 10) : name;
}
return other.get(target);
}

/**
* Creates a new instance by concatenating the specified pointer's path onto this pointer's path.
* @param ptr the string representation of a pointer, it's decoded path, or an instance of JsonPointer indicating the additional path to concatenate onto the pointer.
Expand Down
2 changes: 1 addition & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
export type PathSegment = string | number;
export type PathSegments = readonly PathSegment[];
export type PathSegments = PathSegment[];

export type JsonStringPointer = string;
export type UriFragmentIdentifierPointer = string;
Expand Down
34 changes: 34 additions & 0 deletions src/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,40 @@ export function encodeUriFragmentIdentifier(
return '#/'.concat(encodeFragmentSegments(path).join('/'));
}

const InvalidRelativePointerError =
'Invalid Relative JSON Pointer syntax. Relative pointer must begin with a non-negative integer, followed by either the number sign (#), or a JSON Pointer.';

export function decodeRelativePointer(ptr: JsonStringPointer): PathSegments {
if (typeof ptr !== 'string') {
throw new TypeError(
'Invalid type: Relative JSON Pointers are represented as strings.',
);
}
if (ptr.length === 0) {
// https://tools.ietf.org/id/draft-handrews-relative-json-pointer-00.html#rfc.section.3
throw new ReferenceError(InvalidRelativePointerError);
}
const segments = ptr.split('/');
let first = segments[0];
// It is a name reference; strip the hash.
if (first[first.length - 1] == '#') {
if (segments.length > 1) {
throw new ReferenceError(InvalidRelativePointerError);
}
first = first.substr(0, first.length - 1);
}
let i = -1;
const len = first.length;
while (++i < len) {
if (first[i] < '0' || first[i] > '9') {
throw new ReferenceError(InvalidRelativePointerError);
}
}
const path: unknown[] = decodePointerSegments(segments.slice(1));
path.unshift(segments[0]);
return path as PathSegments;
}

export function toArrayIndexReference(
arr: readonly unknown[],
idx: PathSegment,
Expand Down

0 comments on commit 2485c9e

Please sign in to comment.