Skip to content

Commit 81475be

Browse files
authored
Allow manipulating values other than JSON (#155)
1 parent 46430d0 commit 81475be

File tree

4 files changed

+136
-83
lines changed

4 files changed

+136
-83
lines changed

src/pointer.ts

+63-28
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {JsonConvertible, JsonStructure, JsonValue} from '@croct/json';
1+
import {JsonConvertible} from '@croct/json';
22

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

18+
/**
19+
* A record or array representing the root of a structure.
20+
*/
21+
export type RootStructure = Record<string | number | symbol, any> | any[];
22+
23+
export type RootValue = any;
24+
25+
/**
26+
* A union of all possible values in a structure.
27+
*/
28+
export type ReferencedValue<T> = NestedValue<T>;
29+
30+
/**
31+
* A union of all possible values in a structure, excluding the given type.
32+
*/
33+
type NestedValue<T, U = never> = T | (
34+
T extends object
35+
? T extends U
36+
? NestedValue<Diff<T, U>, U>
37+
: T extends Array<infer I>
38+
? NestedValue<I, U | T>
39+
: NestedValue<T[keyof T], U | T>
40+
: never
41+
);
42+
43+
type Diff<T extends object, M> = M extends infer U
44+
? T extends U
45+
? Exclude<keyof T, keyof U> extends never
46+
? never
47+
: Pick<T, Exclude<keyof T, keyof U>>
48+
: never
49+
: never;
50+
1851
/**
1952
* An error indicating a problem related to JSON pointer operations.
2053
*/
@@ -51,7 +84,7 @@ export class InvalidReferenceError extends JsonPointerError {
5184
/**
5285
* A key-value pair representing a JSON pointer segment and its value.
5386
*/
54-
export type Entry = [JsonPointerSegment | null, JsonValue];
87+
export type Entry<T> = [JsonPointerSegment | null, T];
5588

5689
/**
5790
* An RFC 6901-compliant JSON pointer.
@@ -273,15 +306,15 @@ export class JsonPointer implements JsonConvertible {
273306
/**
274307
* Returns the value at the referenced location.
275308
*
276-
* @param {JsonValue} value The value to read from.
309+
* @param {RootValue} value The value to read from.
277310
*
278-
* @returns {JsonValue} The value at the referenced location.
311+
* @returns {ReferencedValue} The value at the referenced location.
279312
*
280313
* @throws {InvalidReferenceError} If a numeric segment references a non-array value.
281314
* @throws {InvalidReferenceError} If a string segment references an array value.
282315
* @throws {InvalidReferenceError} If there is no value at any level of the pointer.
283316
*/
284-
public get(value: JsonValue): JsonValue {
317+
public get<T extends RootValue>(value: T): ReferencedValue<T> {
285318
const iterator = this.traverse(value);
286319

287320
let result = iterator.next();
@@ -304,11 +337,11 @@ export class JsonPointer implements JsonConvertible {
304337
*
305338
* This method gracefully handles missing values by returning `false`.
306339
*
307-
* @param {JsonStructure} root The value to check if the reference exists in.
340+
* @param {RootValue} root The value to check if the reference exists in.
308341
*
309-
* @returns {JsonValue} Returns `true` if the value exists, `false` otherwise.
342+
* @returns {boolean} Returns `true` if the value exists, `false` otherwise.
310343
*/
311-
public has(root: JsonStructure): boolean {
344+
public has(root: RootValue): boolean {
312345
try {
313346
this.get(root);
314347
} catch {
@@ -321,8 +354,8 @@ export class JsonPointer implements JsonConvertible {
321354
/**
322355
* Sets the value at the referenced location.
323356
*
324-
* @param {JsonStructure} root The value to write to.
325-
* @param {JsonValue} value The value to set at the referenced location.
357+
* @param {RootValue} root The value to write to.
358+
* @param {unknown} value The value to set at the referenced location.
326359
*
327360
* @throws {InvalidReferenceError} If the pointer references the root of the structure.
328361
* @throws {InvalidReferenceError} If a numeric segment references a non-array value.
@@ -331,17 +364,19 @@ export class JsonPointer implements JsonConvertible {
331364
* @throws {InvalidReferenceError} If setting the value to an array would cause it to become
332365
* sparse.
333366
*/
334-
public set(root: JsonStructure, value: JsonValue): void {
367+
public set<T extends RootValue>(root: T, value: unknown): void {
335368
if (this.isRoot()) {
336369
throw new JsonPointerError('Cannot set root value.');
337370
}
338371

339-
const parent = this.getParent().get(root);
372+
const target = this.getParent().get(root);
340373

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

378+
const parent: RootStructure = target;
379+
345380
const segmentIndex = this.segments.length - 1;
346381
const segment = this.segments[segmentIndex];
347382

@@ -381,30 +416,32 @@ export class JsonPointer implements JsonConvertible {
381416
* is a no-op. Pointers referencing array elements remove the element while keeping
382417
* the array dense.
383418
*
384-
* @param {JsonStructure} root The value to write to.
419+
* @param {RootValue} root The value to write to.
385420
*
386-
* @returns {JsonValue} The unset value, or `undefined` if the referenced location
421+
* @returns {ReferencedValue|undefined} The unset value, or `undefined` if the referenced location
387422
* does not exist.
388423
*
389424
* @throws {InvalidReferenceError} If the pointer references the root of the root.
390425
*/
391-
public unset(root: JsonStructure): JsonValue | undefined {
426+
public unset<T extends RootValue>(root: T): ReferencedValue<T> | undefined {
392427
if (this.isRoot()) {
393428
throw new InvalidReferenceError('Cannot unset the root value.');
394429
}
395430

396-
let parent: JsonValue;
431+
let target: ReferencedValue<T>;
397432

398433
try {
399-
parent = this.getParent().get(root);
434+
target = this.getParent().get(root);
400435
} catch {
401436
return undefined;
402437
}
403438

404-
if (typeof parent !== 'object' || parent === null) {
439+
if (typeof target !== 'object' || target === null) {
405440
return undefined;
406441
}
407442

443+
const parent: RootStructure = target;
444+
408445
const segmentIndex = this.segments.length - 1;
409446
const segment = this.segments[segmentIndex];
410447

@@ -434,17 +471,17 @@ export class JsonPointer implements JsonConvertible {
434471
/**
435472
* Returns an iterator over the stack of values that the pointer references.
436473
*
437-
* @param {JsonValue} root The value to traverse.
474+
* @param {RootValue} root The value to traverse.
438475
*
439-
* @returns {Iterator<JsonPointer>} An iterator over the stack of values that the
476+
* @returns {Iterator<Entry<ReferencedValue<T>>} An iterator over the stack of values that the
440477
* pointer references.
441478
*
442479
* @throws {InvalidReferenceError} If a numeric segment references a non-array value.
443480
* @throws {InvalidReferenceError} If a string segment references an array value.
444481
* @throws {InvalidReferenceError} If there is no value at any level of the pointer.
445482
*/
446-
public* traverse(root: JsonValue): Iterator<Entry> {
447-
let current: JsonValue = root;
483+
public* traverse<T extends RootValue>(root: T): Iterator<Entry<ReferencedValue<T>>> {
484+
let current: ReferencedValue<T> = root;
448485

449486
yield [null, current];
450487

@@ -487,15 +524,13 @@ export class JsonPointer implements JsonConvertible {
487524
);
488525
}
489526

490-
const nextValue = current[segment];
491-
492-
if (nextValue === undefined) {
527+
if (!(segment in current)) {
493528
throw new InvalidReferenceError(
494529
`Property "${segment}" does not exist at "${this.truncatedAt(i)}".`,
495530
);
496531
}
497532

498-
current = nextValue;
533+
current = current[segment as keyof typeof current] as ReferencedValue<T>;
499534

500535
yield [segment, current];
501536
}
@@ -508,7 +543,7 @@ export class JsonPointer implements JsonConvertible {
508543
*
509544
* @returns {boolean} `true` if the pointers are logically equal, `false` otherwise.
510545
*/
511-
public equals(other: any): other is this {
546+
public equals(other: unknown): other is JsonPointer {
512547
if (this === other) {
513548
return true;
514549
}

src/relativePointer.ts

+26-21
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {JsonConvertible, JsonStructure, JsonValue} from '@croct/json';
1+
import {JsonConvertible, JsonStructure} from '@croct/json';
22
import {
33
JsonPointer,
44
JsonPointerSegments,
@@ -8,6 +8,8 @@ import {
88
JsonPointerLike,
99
Entry,
1010
InvalidReferenceError,
11+
ReferencedValue,
12+
RootValue,
1113
} from './pointer';
1214

1315
/**
@@ -245,18 +247,18 @@ export class JsonRelativePointer implements JsonConvertible {
245247
/**
246248
* Returns the value at the referenced location.
247249
*
248-
* @param {JsonValue} root The value to read from.
250+
* @param {RootValue} root The value to read from.
249251
* @param {JsonPointer} pointer The base pointer to resolve the current pointer against.
250252
*
251-
* @returns {JsonValue} The value at the referenced location.
253+
* @returns {ReferencedValue|JsonPointerSegment} The value at the referenced location.
252254
*
253255
* @throws {InvalidReferenceError} If a numeric segment references a non-array value.
254256
* @throws {InvalidReferenceError} If a string segment references an array value.
255257
* @throws {InvalidReferenceError} If an array index is out of bounds.
256258
* @throws {InvalidReferenceError} If there is no value at any level of the pointer.
257259
* @throws {InvalidReferenceError} If the pointer references the key of the root value.
258260
*/
259-
public get(root: JsonValue, pointer = JsonPointer.root()): JsonValue {
261+
public get<T extends RootValue>(root: T, pointer = JsonPointer.root()): ReferencedValue<T>|JsonPointerSegment {
260262
const stack = this.getReferenceStack(root, pointer);
261263
const [segment, value] = stack[stack.length - 1];
262264

@@ -268,20 +270,21 @@ export class JsonRelativePointer implements JsonConvertible {
268270
return segment;
269271
}
270272

271-
return this.getRemainderPointer().get(value);
273+
// Given V = typeof value, and typeof value ⊆ ReferencedValue<T> → ReferencedValue<K> ⊆ ReferencedValue<T>
274+
return this.getRemainderPointer().get(value) as ReferencedValue<T>;
272275
}
273276

274277
/**
275278
* Checks whether the value at the referenced location exists.
276279
*
277280
* This method gracefully handles missing values by returning `false`.
278281
*
279-
* @param {JsonValue} root The value to check if the reference exists in.
282+
* @param {RootValue} root The value to check if the reference exists in.
280283
* @param {JsonPointer} pointer The base pointer to resolve the current pointer against.
281284
*
282-
* @returns {JsonValue} Returns `true` if the value exists, `false` otherwise.
285+
* @returns {boolean} Returns `true` if the value exists, `false` otherwise.
283286
*/
284-
public has(root: JsonValue, pointer: JsonPointer = JsonPointer.root()): boolean {
287+
public has(root: RootValue, pointer: JsonPointer = JsonPointer.root()): boolean {
285288
try {
286289
this.get(root, pointer);
287290
} catch {
@@ -294,8 +297,8 @@ export class JsonRelativePointer implements JsonConvertible {
294297
/**
295298
* Sets the value at the referenced location.
296299
*
297-
* @param {JsonValue} root The value to write to.
298-
* @param {JsonValue} value The value to set at the referenced location.
300+
* @param {RootValue} root The value to write to.
301+
* @param {unknown} value The value to set at the referenced location.
299302
* @param {JsonPointer} pointer The base pointer to resolve the current pointer against.
300303
*
301304
* @throws {InvalidReferenceError} If the pointer references the root of the structure.
@@ -306,7 +309,7 @@ export class JsonRelativePointer implements JsonConvertible {
306309
* @throws {InvalidReferenceError} If setting the value to an array would cause it to become
307310
* sparse.
308311
*/
309-
public set(root: JsonValue, value: JsonValue, pointer = JsonPointer.root()): void {
312+
public set(root: RootValue, value: unknown, pointer = JsonPointer.root()): void {
310313
if (this.isKeyPointer()) {
311314
throw new JsonPointerError('Cannot write to a key.');
312315
}
@@ -337,15 +340,15 @@ export class JsonRelativePointer implements JsonConvertible {
337340
* is a no-op. Pointers referencing array elements remove the element while keeping
338341
* the array dense.
339342
*
340-
* @param {JsonValue} root The value to write to.
343+
* @param {RootValue} root The value to write to.
341344
* @param {JsonPointer} pointer The base pointer to resolve the current pointer against.
342345
*
343346
* @returns {JsonValue} The unset value, or `undefined` if the referenced location
344347
* does not exist.
345348
*
346349
* @throws {InvalidReferenceError} If the pointer references the root of the structure.
347350
*/
348-
public unset(root: JsonValue, pointer = JsonPointer.root()): JsonValue | undefined {
351+
public unset<T extends RootValue>(root: T, pointer = JsonPointer.root()): ReferencedValue<T> | undefined {
349352
if (this.isKeyPointer()) {
350353
throw new JsonPointerError('Cannot write to a key.');
351354
}
@@ -354,36 +357,38 @@ export class JsonRelativePointer implements JsonConvertible {
354357
const remainderPointer = this.getRemainderPointer();
355358

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

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

364368
const segment = stack[stack.length - 1][0]!;
365-
const structure = stack[stack.length - 2][1] as JsonStructure;
369+
const parent = stack[stack.length - 2][1];
366370

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

370375
/**
371376
* Returns the stack of references to the value at the referenced location.
372377
*
373-
* @param {JsonValue} root The value to read from.
378+
* @param {RootValue} root The value to read from.
374379
* @param {JsonPointer} pointer The base pointer to resolve the current pointer against.
375380
*
376-
* @returns {Entry[]} The list of entries in top-down order.
381+
* @returns {Entry<ReferencedValue>[]} The list of entries in top-down order.
377382
*
378383
* @throws {InvalidReferenceError} If a numeric segment references a non-array value.
379384
* @throws {InvalidReferenceError} If a string segment references an array value.
380385
* @throws {InvalidReferenceError} If an array index is out of bounds.
381386
* @throws {InvalidReferenceError} If there is no value at any level of the pointer.
382387
*/
383-
private getReferenceStack(root: JsonValue, pointer: JsonPointer): Entry[] {
388+
private getReferenceStack<T extends RootValue>(root: T, pointer: JsonPointer): Array<Entry<ReferencedValue<T>>> {
384389
const iterator = pointer.traverse(root);
385390
let current = iterator.next();
386-
const stack: Entry[] = [];
391+
const stack: Array<Entry<ReferencedValue<T>>> = [];
387392

388393
while (current.done === false) {
389394
stack.push(current.value);
@@ -436,7 +441,7 @@ export class JsonRelativePointer implements JsonConvertible {
436441
*
437442
* @returns {boolean} `true` if the pointers are logically equal, `false` otherwise.
438443
*/
439-
public equals(other: any): other is this {
444+
public equals(other: any): other is JsonRelativePointer {
440445
if (this === other) {
441446
return true;
442447
}

0 commit comments

Comments
 (0)