Skip to content

Allow manipulating values other than JSON #155

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Jan 9, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading