Skip to content

Commit

Permalink
Allow manipulating values other than JSON (#155)
Browse files Browse the repository at this point in the history
  • Loading branch information
marcospassos authored Jan 9, 2025
1 parent 46430d0 commit 81475be
Show file tree
Hide file tree
Showing 4 changed files with 136 additions and 83 deletions.
91 changes: 63 additions & 28 deletions src/pointer.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {JsonConvertible, JsonStructure, JsonValue} from '@croct/json';
import {JsonConvertible} from '@croct/json';

/**
* A value that can be converted to a JSON pointer.
Expand All @@ -15,6 +15,39 @@ export type JsonPointerSegment = string | number;
*/
export type JsonPointerSegments = JsonPointerSegment[];

/**
* A record or array representing the root of a structure.
*/
export type RootStructure = Record<string | number | symbol, any> | any[];

export type RootValue = any;

/**
* A union of all possible values in a structure.
*/
export type ReferencedValue<T> = NestedValue<T>;

/**
* A union of all possible values in a structure, excluding the given type.
*/
type NestedValue<T, U = never> = T | (
T extends object
? T extends U
? NestedValue<Diff<T, U>, U>
: T extends Array<infer I>
? NestedValue<I, U | T>
: NestedValue<T[keyof T], U | T>
: never
);

type Diff<T extends object, M> = M extends infer U
? T extends U
? Exclude<keyof T, keyof U> extends never
? never
: Pick<T, Exclude<keyof T, keyof U>>
: never
: never;

/**
* An error indicating a problem related to JSON pointer operations.
*/
Expand Down Expand Up @@ -51,7 +84,7 @@ export class InvalidReferenceError extends JsonPointerError {
/**
* A key-value pair representing a JSON pointer segment and its value.
*/
export type Entry = [JsonPointerSegment | null, JsonValue];
export type Entry<T> = [JsonPointerSegment | null, T];

/**
* An RFC 6901-compliant JSON pointer.
Expand Down Expand Up @@ -273,15 +306,15 @@ export class JsonPointer implements JsonConvertible {
/**
* Returns the value at the referenced location.
*
* @param {JsonValue} value The value to read from.
* @param {RootValue} value The value to read from.
*
* @returns {JsonValue} The value at the referenced location.
* @returns {ReferencedValue} 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 there is no value at any level of the pointer.
*/
public get(value: JsonValue): JsonValue {
public get<T extends RootValue>(value: T): ReferencedValue<T> {
const iterator = this.traverse(value);

let result = iterator.next();
Expand All @@ -304,11 +337,11 @@ export class JsonPointer implements JsonConvertible {
*
* This method gracefully handles missing values by returning `false`.
*
* @param {JsonStructure} root The value to check if the reference exists in.
* @param {RootValue} root The value to check if the reference exists in.
*
* @returns {JsonValue} Returns `true` if the value exists, `false` otherwise.
* @returns {boolean} Returns `true` if the value exists, `false` otherwise.
*/
public has(root: JsonStructure): boolean {
public has(root: RootValue): boolean {
try {
this.get(root);
} catch {
Expand All @@ -321,8 +354,8 @@ export class JsonPointer implements JsonConvertible {
/**
* Sets the value at the referenced location.
*
* @param {JsonStructure} root The value to write to.
* @param {JsonValue} value The value to set at the referenced location.
* @param {RootValue} root The value to write to.
* @param {unknown} value The value to set at the referenced location.
*
* @throws {InvalidReferenceError} If the pointer references the root of the structure.
* @throws {InvalidReferenceError} If a numeric segment references a non-array value.
Expand All @@ -331,17 +364,19 @@ export class JsonPointer implements JsonConvertible {
* @throws {InvalidReferenceError} If setting the value to an array would cause it to become
* sparse.
*/
public set(root: JsonStructure, value: JsonValue): void {
public set<T extends RootValue>(root: T, value: unknown): void {
if (this.isRoot()) {
throw new JsonPointerError('Cannot set root value.');
}

const parent = this.getParent().get(root);
const target = this.getParent().get(root);

if (typeof parent !== 'object' || parent === null) {
if (typeof target !== 'object' || target === null) {
throw new JsonPointerError(`Cannot set value at "${this.getParent()}".`);
}

const parent: RootStructure = target;

const segmentIndex = this.segments.length - 1;
const segment = this.segments[segmentIndex];

Expand Down Expand Up @@ -381,30 +416,32 @@ export class JsonPointer implements JsonConvertible {
* is a no-op. Pointers referencing array elements remove the element while keeping
* the array dense.
*
* @param {JsonStructure} root The value to write to.
* @param {RootValue} root The value to write to.
*
* @returns {JsonValue} The unset value, or `undefined` if the referenced location
* @returns {ReferencedValue|undefined} The unset value, or `undefined` if the referenced location
* does not exist.
*
* @throws {InvalidReferenceError} If the pointer references the root of the root.
*/
public unset(root: JsonStructure): JsonValue | undefined {
public unset<T extends RootValue>(root: T): ReferencedValue<T> | undefined {
if (this.isRoot()) {
throw new InvalidReferenceError('Cannot unset the root value.');
}

let parent: JsonValue;
let target: ReferencedValue<T>;

try {
parent = this.getParent().get(root);
target = this.getParent().get(root);
} catch {
return undefined;
}

if (typeof parent !== 'object' || parent === null) {
if (typeof target !== 'object' || target === null) {
return undefined;
}

const parent: RootStructure = target;

const segmentIndex = this.segments.length - 1;
const segment = this.segments[segmentIndex];

Expand Down Expand Up @@ -434,17 +471,17 @@ export class JsonPointer implements JsonConvertible {
/**
* Returns an iterator over the stack of values that the pointer references.
*
* @param {JsonValue} root The value to traverse.
* @param {RootValue} root The value to traverse.
*
* @returns {Iterator<JsonPointer>} An iterator over the stack of values that the
* @returns {Iterator<Entry<ReferencedValue<T>>} An iterator over the stack of values that the
* pointer references.
*
* @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.
*/
public* traverse(root: JsonValue): Iterator<Entry> {
let current: JsonValue = root;
public* traverse<T extends RootValue>(root: T): Iterator<Entry<ReferencedValue<T>>> {
let current: ReferencedValue<T> = root;

yield [null, current];

Expand Down Expand Up @@ -487,15 +524,13 @@ export class JsonPointer implements JsonConvertible {
);
}

const nextValue = current[segment];

if (nextValue === undefined) {
if (!(segment in current)) {
throw new InvalidReferenceError(
`Property "${segment}" does not exist at "${this.truncatedAt(i)}".`,
);
}

current = nextValue;
current = current[segment as keyof typeof current] as ReferencedValue<T>;

yield [segment, current];
}
Expand All @@ -508,7 +543,7 @@ export class JsonPointer implements JsonConvertible {
*
* @returns {boolean} `true` if the pointers are logically equal, `false` otherwise.
*/
public equals(other: any): other is this {
public equals(other: unknown): other is JsonPointer {
if (this === other) {
return true;
}
Expand Down
47 changes: 26 additions & 21 deletions src/relativePointer.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {JsonConvertible, JsonStructure, JsonValue} from '@croct/json';
import {JsonConvertible, JsonStructure} from '@croct/json';
import {
JsonPointer,
JsonPointerSegments,
Expand All @@ -8,6 +8,8 @@ import {
JsonPointerLike,
Entry,
InvalidReferenceError,
ReferencedValue,
RootValue,
} from './pointer';

/**
Expand Down Expand Up @@ -245,18 +247,18 @@ export class JsonRelativePointer implements JsonConvertible {
/**
* Returns the value at the referenced location.
*
* @param {JsonValue} root The value to read from.
* @param {RootValue} root The value to read from.
* @param {JsonPointer} pointer The base pointer to resolve the current pointer against.
*
* @returns {JsonValue} The value at the referenced location.
* @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.
*/
public get(root: JsonValue, pointer = JsonPointer.root()): JsonValue {
public get<T extends RootValue>(root: T, pointer = JsonPointer.root()): ReferencedValue<T>|JsonPointerSegment {
const stack = this.getReferenceStack(root, pointer);
const [segment, value] = stack[stack.length - 1];

Expand All @@ -268,20 +270,21 @@ export class JsonRelativePointer implements JsonConvertible {
return segment;
}

return this.getRemainderPointer().get(value);
// Given V = typeof value, and typeof value ⊆ ReferencedValue<T> → ReferencedValue<K> ⊆ ReferencedValue<T>
return this.getRemainderPointer().get(value) as ReferencedValue<T>;
}

/**
* Checks whether the value at the referenced location exists.
*
* This method gracefully handles missing values by returning `false`.
*
* @param {JsonValue} root The value to check if the reference exists in.
* @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 {JsonValue} Returns `true` if the value exists, `false` otherwise.
* @returns {boolean} Returns `true` if the value exists, `false` otherwise.
*/
public has(root: JsonValue, pointer: JsonPointer = JsonPointer.root()): boolean {
public has(root: RootValue, pointer: JsonPointer = JsonPointer.root()): boolean {
try {
this.get(root, pointer);
} catch {
Expand All @@ -294,8 +297,8 @@ export class JsonRelativePointer implements JsonConvertible {
/**
* Sets the value at the referenced location.
*
* @param {JsonValue} root The value to write to.
* @param {JsonValue} value The value to set 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.
Expand All @@ -306,7 +309,7 @@ export class JsonRelativePointer implements JsonConvertible {
* @throws {InvalidReferenceError} If setting the value to an array would cause it to become
* sparse.
*/
public set(root: JsonValue, value: JsonValue, pointer = JsonPointer.root()): void {
public set(root: RootValue, value: unknown, pointer = JsonPointer.root()): void {
if (this.isKeyPointer()) {
throw new JsonPointerError('Cannot write to a key.');
}
Expand Down Expand Up @@ -337,15 +340,15 @@ export class JsonRelativePointer implements JsonConvertible {
* is a no-op. Pointers referencing array elements remove the element while keeping
* the array dense.
*
* @param {JsonValue} root The value to write to.
* @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.
*/
public unset(root: JsonValue, pointer = JsonPointer.root()): JsonValue | undefined {
public unset<T extends RootValue>(root: T, pointer = JsonPointer.root()): ReferencedValue<T> | undefined {
if (this.isKeyPointer()) {
throw new JsonPointerError('Cannot write to a key.');
}
Expand All @@ -354,36 +357,38 @@ export class JsonRelativePointer implements JsonConvertible {
const remainderPointer = this.getRemainderPointer();

if (!remainderPointer.isRoot()) {
return remainderPointer.unset(stack[stack.length - 1][1] as JsonStructure);
// Given V = typeof value, and typeof value ⊆ ReferencedValue<T> → ReferencedValue<K> ⊆ ReferencedValue<T>
return remainderPointer.unset(stack[stack.length - 1][1]) as ReferencedValue<T>;
}

if (stack.length < 2) {
throw new JsonPointerError('Cannot unset the root value.');
}

const segment = stack[stack.length - 1][0]!;
const structure = stack[stack.length - 2][1] as JsonStructure;
const parent = stack[stack.length - 2][1];

return JsonPointer.from([segment]).unset(structure);
// Given V = typeof value, and typeof value ⊆ ReferencedValue<T> → ReferencedValue<K> ⊆ ReferencedValue<T>
return JsonPointer.from([segment]).unset(parent) as ReferencedValue<T>;
}

/**
* Returns the stack of references to the value at the referenced location.
*
* @param {JsonValue} root The value to read from.
* @param {RootValue} root The value to read from.
* @param {JsonPointer} pointer The base pointer to resolve the current pointer against.
*
* @returns {Entry[]} The list of entries in top-down order.
* @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.
*/
private getReferenceStack(root: JsonValue, pointer: JsonPointer): Entry[] {
private getReferenceStack<T extends RootValue>(root: T, pointer: JsonPointer): Array<Entry<ReferencedValue<T>>> {
const iterator = pointer.traverse(root);
let current = iterator.next();
const stack: Entry[] = [];
const stack: Array<Entry<ReferencedValue<T>>> = [];

while (current.done === false) {
stack.push(current.value);
Expand Down Expand Up @@ -436,7 +441,7 @@ export class JsonRelativePointer implements JsonConvertible {
*
* @returns {boolean} `true` if the pointers are logically equal, `false` otherwise.
*/
public equals(other: any): other is this {
public equals(other: any): other is JsonRelativePointer {
if (this === other) {
return true;
}
Expand Down
Loading

0 comments on commit 81475be

Please sign in to comment.