@croct/json-pointer
Version:
A RFC6901 compliant type-safe JSON pointer library to handle arbitrary structured data.
395 lines (394 loc) • 15.8 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.JsonRelativePointer = void 0;
const pointer_1 = require("./pointer");
/**
* A relative JSON pointer.
*
* @see https://datatracker.ietf.org/doc/html/draft-bhutton-relative-json-pointer-00
*/
class JsonRelativePointer {
constructor(segments) {
this.segments = segments;
}
/**
* Creates a pointer from any valid pointer-like value.
*
* The return is as follows:
*
* - Pointers are returned as given
* - Numbers are used as the number of parent levels
* - Arrays are assumed to be unescaped segments
* - Strings are delegated to `JsonRelativePointer.parse` and the result is returned
*
* @param path A pointer-like value.
*
* @returns The normalized pointer for the given value.
*
* @see JsonRelativePointer.parse
*/
static from(path) {
if (path instanceof JsonRelativePointer) {
return path;
}
if (Array.isArray(path)) {
if (path.length > 0 && !/^\d+([+-]\d+)?#?$/.test(path[0].toString())) {
throw new pointer_1.InvalidSyntaxError('A relative JSON pointer must start with a non-negative '
+ 'integer optionally followed by a hash character.');
}
return JsonRelativePointer.fromSegments(path);
}
if (typeof path === 'number') {
return JsonRelativePointer.fromSegments([path]);
}
return JsonRelativePointer.parse(path);
}
/**
* Creates a pointer from a list of unescaped segments.
*
* Numeric segments must be safe non-negative integers.
*
* @param {JsonPointerSegments} segments A list of unescaped segments.
*
* @returns {JsonPointer} The pointer to the value at the path specified by the segments.
*
* @throws {InvalidSyntaxError} If the segments are not valid.
*/
static fromSegments(segments) {
if (segments.length === 0) {
throw new pointer_1.InvalidSyntaxError('A relative pointer must have at least one segment.');
}
return new JsonRelativePointer(pointer_1.JsonPointer.from(segments).getSegments());
}
/**
* Parses a string into a pointer. The string is split on the dot character and
* segments composed solely of numbers are parsed as integers.
*
* @param {string} path The string representation of a pointer.
*
* @returns {JsonPointer} The pointer to the value at the specified path.
*
* @throws {InvalidSyntaxError} If the path is not a valid JSON Pointer.
*/
static parse(path) {
if (!/^\d+([+-]\d+)?#?(\/|$)/.test(path)) {
throw new pointer_1.InvalidSyntaxError('A relative JSON pointer must start with a non-negative '
+ 'integer optionally followed by a hash character.');
}
return JsonRelativePointer.fromSegments(pointer_1.JsonPointer.parse(`/${path}`).getSegments());
}
/**
* Checks whether the pointer references a key (array index or object property).
*
* @returns {boolean} Whether the pointer references key.
*/
isKeyPointer() {
return `${this.segments[0]}`.endsWith('#');
}
/**
* Returns the number of levels up from the initial location.
*
* @returns {number} The number of levels up from the initial location.
*/
getParentIndex() {
return Number.parseInt(`${this.segments[0]}`, 10);
}
/**
* Returns the index offset of the pointer.
*
* @returns {number} A signed integer representing the offset of the pointer.
*/
getParentIndexOffset() {
const match = /([+-]\d+)#?$/.exec((`${this.segments[0]}`));
if (match === null) {
return 0;
}
return Number.parseInt(match[1], 10);
}
/**
* Returns a pointer to the parent of the current pointer.
*
* @returns {JsonPointer} The parent pointer.
*/
getParent() {
if (this.segments.length < 2) {
throw new pointer_1.JsonPointerError('Cannot get the parent of a unresolved segment.');
}
return new JsonRelativePointer(this.segments.slice(0, -1));
}
/**
* Returns a pointer that represents the remainder of the path after the first segment.
*
* For example, if the current pointer is `0/foo/bar/baz` calling this method returns
* a pointer to `/foo/bar/baz`.
*
* @returns {JsonPointer} A pointer to the remainder of the path.
*/
getRemainderPointer() {
return pointer_1.JsonPointer.from(this.segments.slice(1));
}
/**
* Returns the segments of the pointer.
*
* @returns {JsonPointerSegments} The segments of the pointer.
*/
getSegments() {
return [...this.segments];
}
/**
* Joins this pointer with another one and returns the result.
*
* The segments of the second pointer are appended to the segments of the first.
*
* These are equivalent:
*
* ```js
* JsonRelativePointer.from([1, 'bar']).join(JsonPointer.from(['baz']))
* JsonRelativePointer.from(['1', 'bar', 'baz'])
* ```
*
* @param {JsonPointer} other The pointer to append to this one.
*
* @returns {JsonPointer} A pointer with the segments of this and the other pointer joined.
*/
joinedWith(other) {
if (this.isKeyPointer()) {
throw new pointer_1.JsonPointerError('Cannot join a key pointer.');
}
const normalizedPointer = pointer_1.JsonPointer.from(other);
if (normalizedPointer.isRoot()) {
return this;
}
return JsonRelativePointer.fromSegments(this.segments.concat(normalizedPointer.getSegments()));
}
/**
* Resolves relative pointer from an absolute pointer.
*
* @param {JsonPointerLike} pointer The base pointer.
*
* @returns {JsonPointer} The resolved pointer.
*
* @throws {JsonPointerError} If the pointer is out of bounds.
* @throws {JsonPointerError} If the pointer is a key pointer.
* @throws {JsonPointerError} If the pointer includes index offsets.
*/
resolve(pointer) {
if (this.isKeyPointer()) {
throw new pointer_1.JsonPointerError('A key pointer cannot be resolved to an absolute pointer.');
}
if (this.getParentIndexOffset() !== 0) {
throw new pointer_1.JsonPointerError('A pointer with an offset cannot be resolved to an absolute pointer.');
}
const parentIndex = this.getParentIndex();
const base = pointer_1.JsonPointer.from(pointer);
const segments = base.getSegments();
if (parentIndex > segments.length) {
throw new pointer_1.JsonPointerError('The relative pointer is out of bounds.');
}
return pointer_1.JsonPointer.from([
...(parentIndex > 0 ? segments.slice(0, -parentIndex) : segments),
...this.segments.slice(1),
]);
}
/**
* Returns the value at the referenced location.
*
* @param {RootValue} root The value to read from.
* @param {JsonPointer} pointer The base pointer to resolve the current pointer against.
*
* @returns {ReferencedValue|JsonPointerSegment} The value at the referenced location.
*
* @throws {InvalidReferenceError} If a numeric segment references a non-array value.
* @throws {InvalidReferenceError} If a string segment references an array value.
* @throws {InvalidReferenceError} If an array index is out of bounds.
* @throws {InvalidReferenceError} If there is no value at any level of the pointer.
* @throws {InvalidReferenceError} If the pointer references the key of the root value.
*/
get(root, pointer = pointer_1.JsonPointer.root()) {
const stack = this.getReferenceStack(root, pointer);
const [segment, value] = stack[stack.length - 1];
if (this.isKeyPointer()) {
if (segment === null) {
throw new pointer_1.InvalidReferenceError('The root value has no key.');
}
return segment;
}
// Given V = typeof value, and typeof value ⊆ ReferencedValue<T> → ReferencedValue<K> ⊆ ReferencedValue<T>
return this.getRemainderPointer().get(value);
}
/**
* Checks whether the value at the referenced location exists.
*
* This method gracefully handles missing values by returning `false`.
*
* @param {RootValue} root The value to check if the reference exists in.
* @param {JsonPointer} pointer The base pointer to resolve the current pointer against.
*
* @returns {boolean} Returns `true` if the value exists, `false` otherwise.
*/
has(root, pointer = pointer_1.JsonPointer.root()) {
try {
this.get(root, pointer);
}
catch {
return false;
}
return true;
}
/**
* Sets the value at the referenced location.
*
* @param {RootValue} root The value to write to.
* @param {unknown} value The value to set at the referenced location.
* @param {JsonPointer} pointer The base pointer to resolve the current pointer against.
*
* @throws {InvalidReferenceError} If the pointer references the root of the structure.
* @throws {InvalidReferenceError} If a numeric segment references a non-array value.
* @throws {InvalidReferenceError} If a string segment references an array value.
* @throws {InvalidReferenceError} If there is no value at any level of the pointer.
* @throws {InvalidReferenceError} If an array index is out of bounds.
* @throws {InvalidReferenceError} If setting the value to an array would cause it to become
* sparse.
*/
set(root, value, pointer = pointer_1.JsonPointer.root()) {
if (this.isKeyPointer()) {
throw new pointer_1.JsonPointerError('Cannot write to a key.');
}
const stack = this.getReferenceStack(root, pointer);
const remainderPointer = this.getRemainderPointer();
if (!remainderPointer.isRoot()) {
remainderPointer.set(stack[stack.length - 1][1], value);
return;
}
if (stack.length < 2) {
throw new pointer_1.JsonPointerError('Cannot set the root value.');
}
const segment = stack[stack.length - 1][0];
const structure = stack[stack.length - 2][1];
pointer_1.JsonPointer.from([segment]).set(structure, value);
}
/**
* Unsets the value at the referenced location and returns the unset value.
*
* If the given location does not exist, the method returns `undefined`, meaning the call
* is a no-op. Pointers referencing array elements remove the element while keeping
* the array dense.
*
* @param {RootValue} root The value to write to.
* @param {JsonPointer} pointer The base pointer to resolve the current pointer against.
*
* @returns {JsonValue} The unset value, or `undefined` if the referenced location
* does not exist.
*
* @throws {InvalidReferenceError} If the pointer references the root of the structure.
*/
unset(root, pointer = pointer_1.JsonPointer.root()) {
if (this.isKeyPointer()) {
throw new pointer_1.JsonPointerError('Cannot write to a key.');
}
const stack = this.getReferenceStack(root, pointer);
const remainderPointer = this.getRemainderPointer();
if (!remainderPointer.isRoot()) {
// Given V = typeof value, and typeof value ⊆ ReferencedValue<T> → ReferencedValue<K> ⊆ ReferencedValue<T>
return remainderPointer.unset(stack[stack.length - 1][1]);
}
if (stack.length < 2) {
throw new pointer_1.JsonPointerError('Cannot unset the root value.');
}
const segment = stack[stack.length - 1][0];
const parent = stack[stack.length - 2][1];
// Given V = typeof value, and typeof value ⊆ ReferencedValue<T> → ReferencedValue<K> ⊆ ReferencedValue<T>
return pointer_1.JsonPointer.from([segment]).unset(parent);
}
/**
* Returns the stack of references to the value at the referenced location.
*
* @param {RootValue} root The value to read from.
* @param {JsonPointer} pointer The base pointer to resolve the current pointer against.
*
* @returns {Entry<ReferencedValue>[]} The list of entries in top-down order.
*
* @throws {InvalidReferenceError} If a numeric segment references a non-array value.
* @throws {InvalidReferenceError} If a string segment references an array value.
* @throws {InvalidReferenceError} If an array index is out of bounds.
* @throws {InvalidReferenceError} If there is no value at any level of the pointer.
*/
getReferenceStack(root, pointer) {
const iterator = pointer.traverse(root);
let current = iterator.next();
const stack = [];
while (current.done === false) {
stack.push(current.value);
const next = iterator.next();
if (next.done === true) {
break;
}
current = next;
}
const parentIndex = this.getParentIndex();
if (parentIndex >= stack.length) {
throw new pointer_1.JsonPointerError('The relative pointer is out of bounds.');
}
const stackIndex = stack.length - parentIndex - 1;
const offset = this.getParentIndexOffset();
if (offset !== 0) {
const entry = stack[stackIndex];
const elements = stack[stackIndex - 1]?.[1];
if (!Array.isArray(elements) || typeof entry[0] !== 'number') {
throw new pointer_1.InvalidReferenceError('An offset can only be applied to array elements.');
}
const offsetIndex = entry[0] + offset;
if (offsetIndex < 0 || offsetIndex >= elements.length) {
throw new pointer_1.InvalidReferenceError('The element index is out of bounds.');
}
return [
...stack.slice(0, stackIndex),
[offsetIndex, elements[offsetIndex]],
];
}
return stack.slice(0, stackIndex + 1);
}
/**
* Checks whether the pointer is logically equivalent to another pointer.
*
* @param {any} other The pointer to check for equality.
*
* @returns {boolean} `true` if the pointers are logically equal, `false` otherwise.
*/
equals(other) {
if (this === other) {
return true;
}
if (!(other instanceof JsonRelativePointer)) {
return false;
}
if (this.segments.length !== other.segments.length) {
return false;
}
for (let i = 0; i < this.segments.length; i++) {
if (this.segments[i] !== other.segments[i]) {
return false;
}
}
return true;
}
/**
* Returns the string representation of the pointer.
*
* @returns {string} The string representation of the pointer
*/
toJSON() {
return this.toString();
}
/**
* Returns the string representation of the pointer.
*
* @returns {string} The string representation of the pointer
*/
toString() {
const parentSegment = this.segments[0].toString();
const remainingSegments = pointer_1.JsonPointer.from(this.segments.slice(1));
return `${parentSegment}${remainingSegments}`;
}
}
exports.JsonRelativePointer = JsonRelativePointer;