1
1
import { JsonConvertible , JsonStructure , JsonValue } from '@croct/json' ;
2
2
3
+ /**
4
+ * A value that can be converted to a JSON pointer.
5
+ */
3
6
export type JsonPointerLike = JsonPointer | number | string | JsonPointerSegments ;
4
7
8
+ /**
9
+ * A JSON pointer segment.
10
+ */
5
11
export type JsonPointerSegment = string | number ;
6
12
13
+ /**
14
+ * A list of JSON pointer segments.
15
+ */
7
16
export type JsonPointerSegments = JsonPointerSegment [ ] ;
8
17
9
18
/**
@@ -39,16 +48,32 @@ export class InvalidReferenceError extends JsonPointerError {
39
48
}
40
49
}
41
50
51
+ /**
52
+ * A key-value pair representing a JSON pointer segment and its value.
53
+ */
54
+ export type Entry = [ JsonPointerSegment | null , JsonValue ] ;
55
+
42
56
/**
43
57
* An RFC 6901-compliant JSON pointer.
44
58
*
45
59
* @see https://tools.ietf.org/html/rfc6901
46
60
*/
47
61
export class JsonPointer implements JsonConvertible {
62
+ /**
63
+ * A singleton representing the root pointer.
64
+ */
48
65
private static readonly ROOT_SINGLETON = new JsonPointer ( [ ] ) ;
49
66
67
+ /**
68
+ * The list of segments that form the pointer.
69
+ */
50
70
private readonly segments : JsonPointerSegments ;
51
71
72
+ /**
73
+ * Initializes a new pointer from a list of segments.
74
+ *
75
+ * @param segments A list of segments.
76
+ */
52
77
private constructor ( segments : JsonPointerSegments ) {
53
78
this . segments = segments ;
54
79
}
@@ -72,7 +97,7 @@ export class JsonPointer implements JsonConvertible {
72
97
* - Pointers are returned as given
73
98
* - Numbers are used as single segments
74
99
* - Arrays are assumed to be unescaped segments
75
- * - Strings are delegated to `Pointer .parse` and the result is returned
100
+ * - Strings are delegated to `JsonPointer .parse` and the result is returned
76
101
*
77
102
* @param path A pointer-like value.
78
103
*
@@ -86,7 +111,7 @@ export class JsonPointer implements JsonConvertible {
86
111
}
87
112
88
113
if ( Array . isArray ( path ) ) {
89
- return JsonPointer . fromSegments ( path ) ;
114
+ return JsonPointer . fromSegments ( path . map ( JsonPointer . normalizeSegment ) ) ;
90
115
}
91
116
92
117
if ( typeof path === 'number' ) {
@@ -99,7 +124,7 @@ export class JsonPointer implements JsonConvertible {
99
124
/**
100
125
* Creates a pointer from a list of unescaped segments.
101
126
*
102
- * Numeric segments must be finite non-negative integers.
127
+ * Numeric segments must be safe non-negative integers.
103
128
*
104
129
* @param {JsonPointerSegments } segments A list of unescaped segments.
105
130
*
@@ -146,9 +171,9 @@ export class JsonPointer implements JsonConvertible {
146
171
}
147
172
148
173
/**
149
- * Checks whether the reference points to an array element.
174
+ * Checks whether the pointer references an array element.
150
175
*
151
- * @returns {boolean } Whether the pointer is an array index.
176
+ * @returns {boolean } Whether the pointer references an array index.
152
177
*/
153
178
public isIndex ( ) : boolean {
154
179
return typeof this . segments [ this . segments . length - 1 ] === 'number' ;
@@ -161,7 +186,7 @@ export class JsonPointer implements JsonConvertible {
161
186
*
162
187
* @example
163
188
* // returns 2
164
- * Pointer .from('/foo/bar').depth()
189
+ * JsonPointer .from('/foo/bar').depth()
165
190
*
166
191
* @returns {number } The depth of the pointer.
167
192
*/
@@ -225,8 +250,8 @@ export class JsonPointer implements JsonConvertible {
225
250
* These are equivalent:
226
251
*
227
252
* ```js
228
- * Pointer .from(['foo', 'bar']).join(Pointer .from(['baz']))
229
- * Pointer .from(['foo', 'bar', 'baz'])
253
+ * JsonPointer .from(['foo', 'bar']).joinedWith(JsonPointer .from(['baz']))
254
+ * JsonPointer .from(['foo', 'bar', 'baz'])
230
255
* ```
231
256
*
232
257
* @param {JsonPointer } other The pointer to append to this one.
@@ -246,78 +271,44 @@ export class JsonPointer implements JsonConvertible {
246
271
/**
247
272
* Returns the value at the referenced location.
248
273
*
249
- * @param {JsonStructure } structure The structure to get the value from.
274
+ * @param {JsonValue } value The value to read from.
250
275
*
251
276
* @returns {JsonValue } The value at the referenced location.
252
277
*
253
278
* @throws {InvalidReferenceError } If a numeric segment references a non-array value.
254
279
* @throws {InvalidReferenceError } If a string segment references an array value.
255
280
* @throws {InvalidReferenceError } If there is no value at any level of the pointer.
256
281
*/
257
- public get ( structure : JsonStructure ) : JsonValue {
258
- let current : JsonValue = structure ;
259
-
260
- for ( let i = 0 ; i < this . segments . length ; i ++ ) {
261
- if ( typeof current !== 'object' || current === null ) {
262
- throw new InvalidReferenceError ( `Cannot read value at "${ this . truncatedAt ( i ) } ".` ) ;
263
- }
264
-
265
- const segment = this . segments [ i ] ;
266
-
267
- if ( Array . isArray ( current ) ) {
268
- if ( segment === '-' ) {
269
- throw new InvalidReferenceError (
270
- `Index ${ current . length } is out of bounds at "${ this . truncatedAt ( i ) } ".` ,
271
- ) ;
272
- }
273
-
274
- if ( typeof segment !== 'number' ) {
275
- throw new InvalidReferenceError (
276
- `Expected an object at "${ this . truncatedAt ( i ) } ", got an array.` ,
277
- ) ;
278
- }
279
-
280
- if ( segment >= current . length ) {
281
- throw new InvalidReferenceError (
282
- `Index ${ segment } is out of bounds at "${ this . truncatedAt ( i ) } ".` ,
283
- ) ;
284
- }
285
-
286
- current = current [ segment ] ;
282
+ public get ( value : JsonValue ) : JsonValue {
283
+ const iterator = this . traverse ( value ) ;
287
284
288
- continue ;
289
- }
285
+ let result = iterator . next ( ) ;
290
286
291
- if ( typeof segment === 'number' ) {
292
- throw new InvalidReferenceError (
293
- `Expected array at "${ this . truncatedAt ( i ) } ", got object.` ,
294
- ) ;
295
- }
287
+ while ( result . done === false ) {
288
+ const next = iterator . next ( ) ;
296
289
297
- if ( ! ( segment in current ) ) {
298
- throw new InvalidReferenceError (
299
- `Property "${ segment } " does not exist at "${ this . truncatedAt ( i ) } ".` ,
300
- ) ;
290
+ if ( next . done !== false ) {
291
+ break ;
301
292
}
302
293
303
- current = current [ segment ] ;
294
+ result = next ;
304
295
}
305
296
306
- return current ;
297
+ return result . value [ 1 ] ;
307
298
}
308
299
309
300
/**
310
301
* Checks whether the value at the referenced location exists.
311
302
*
312
303
* This method gracefully handles missing values by returning `false`.
313
304
*
314
- * @param {JsonStructure } structure The structure to check if the value exists.
305
+ * @param {JsonStructure } root The value to check if the reference exists in .
315
306
*
316
307
* @returns {JsonValue } Returns `true` if the value exists, `false` otherwise.
317
308
*/
318
- public has ( structure : JsonStructure ) : boolean {
309
+ public has ( root : JsonStructure ) : boolean {
319
310
try {
320
- this . get ( structure ) ;
311
+ this . get ( root ) ;
321
312
} catch {
322
313
return false ;
323
314
}
@@ -328,8 +319,8 @@ export class JsonPointer implements JsonConvertible {
328
319
/**
329
320
* Sets the value at the referenced location.
330
321
*
331
- * @param {JsonStructure } structure The structure to set the value at the referenced location .
332
- * @param {JsonValue } value The value to set.
322
+ * @param {JsonStructure } root The value to write to .
323
+ * @param {JsonValue } value The value to set at the referenced location .
333
324
*
334
325
* @throws {InvalidReferenceError } If the pointer references the root of the structure.
335
326
* @throws {InvalidReferenceError } If a numeric segment references a non-array value.
@@ -338,12 +329,12 @@ export class JsonPointer implements JsonConvertible {
338
329
* @throws {InvalidReferenceError } If setting the value to an array would cause it to become
339
330
* sparse.
340
331
*/
341
- public set ( structure : JsonStructure , value : JsonValue ) : void {
332
+ public set ( root : JsonStructure , value : JsonValue ) : void {
342
333
if ( this . isRoot ( ) ) {
343
334
throw new JsonPointerError ( 'Cannot set root value.' ) ;
344
335
}
345
336
346
- const parent = this . getParent ( ) . get ( structure ) ;
337
+ const parent = this . getParent ( ) . get ( root ) ;
347
338
348
339
if ( typeof parent !== 'object' || parent === null ) {
349
340
throw new JsonPointerError ( `Cannot set value at "${ this . getParent ( ) } ".` ) ;
@@ -388,22 +379,22 @@ export class JsonPointer implements JsonConvertible {
388
379
* is a no-op. Pointers referencing array elements remove the element while keeping
389
380
* the array dense.
390
381
*
391
- * @param {JsonStructure } structure The structure to unset the value at the referenced location .
382
+ * @param {JsonStructure } root The value to write to .
392
383
*
393
384
* @returns {JsonValue } The unset value, or `undefined` if the referenced location
394
385
* does not exist.
395
386
*
396
- * @throws {InvalidReferenceError } If the pointer references the root of the structure .
387
+ * @throws {InvalidReferenceError } If the pointer references the root of the root .
397
388
*/
398
- public unset ( structure : JsonStructure ) : JsonValue | undefined {
389
+ public unset ( root : JsonStructure ) : JsonValue | undefined {
399
390
if ( this . isRoot ( ) ) {
400
391
throw new InvalidReferenceError ( 'Cannot unset the root value.' ) ;
401
392
}
402
393
403
394
let parent : JsonValue ;
404
395
405
396
try {
406
- parent = this . getParent ( ) . get ( structure ) ;
397
+ parent = this . getParent ( ) . get ( root ) ;
407
398
} catch {
408
399
return undefined ;
409
400
}
@@ -438,6 +429,74 @@ export class JsonPointer implements JsonConvertible {
438
429
return value ;
439
430
}
440
431
432
+ /**
433
+ * Returns an iterator over the stack of values that the pointer references.
434
+ *
435
+ * @param {JsonValue } root The value to traverse.
436
+ *
437
+ * @returns {Iterator<JsonPointer> } An iterator over the stack of values that the
438
+ * pointer references.
439
+ *
440
+ * @throws {InvalidReferenceError } If a numeric segment references a non-array value.
441
+ * @throws {InvalidReferenceError } If a string segment references an array value.
442
+ * @throws {InvalidReferenceError } If there is no value at any level of the pointer.
443
+ */
444
+ public * traverse ( root : JsonValue ) : Iterator < Entry > {
445
+ let current : JsonValue = root ;
446
+
447
+ yield [ null , current ] ;
448
+
449
+ for ( let i = 0 ; i < this . segments . length ; i ++ ) {
450
+ if ( typeof current !== 'object' || current === null ) {
451
+ throw new InvalidReferenceError ( `Cannot read value at "${ this . truncatedAt ( i ) } ".` ) ;
452
+ }
453
+
454
+ const segment = this . segments [ i ] ;
455
+
456
+ if ( Array . isArray ( current ) ) {
457
+ if ( segment === '-' ) {
458
+ throw new InvalidReferenceError (
459
+ `Index ${ current . length } is out of bounds at "${ this . truncatedAt ( i ) } ".` ,
460
+ ) ;
461
+ }
462
+
463
+ if ( typeof segment !== 'number' ) {
464
+ throw new InvalidReferenceError (
465
+ `Expected an object at "${ this . truncatedAt ( i ) } ", got an array.` ,
466
+ ) ;
467
+ }
468
+
469
+ if ( segment >= current . length ) {
470
+ throw new InvalidReferenceError (
471
+ `Index ${ segment } is out of bounds at "${ this . truncatedAt ( i ) } ".` ,
472
+ ) ;
473
+ }
474
+
475
+ current = current [ segment ] ;
476
+
477
+ yield [ segment , current ] ;
478
+
479
+ continue ;
480
+ }
481
+
482
+ if ( typeof segment === 'number' ) {
483
+ throw new InvalidReferenceError (
484
+ `Expected array at "${ this . truncatedAt ( i ) } ", got object.` ,
485
+ ) ;
486
+ }
487
+
488
+ if ( ! ( segment in current ) ) {
489
+ throw new InvalidReferenceError (
490
+ `Property "${ segment } " does not exist at "${ this . truncatedAt ( i ) } ".` ,
491
+ ) ;
492
+ }
493
+
494
+ current = current [ segment ] ;
495
+
496
+ yield [ segment , current ] ;
497
+ }
498
+ }
499
+
441
500
/**
442
501
* Checks whether the pointer is logically equivalent to another pointer.
443
502
*
@@ -489,14 +548,31 @@ export class JsonPointer implements JsonConvertible {
489
548
return `/${ this . segments . map ( JsonPointer . escapeSegment ) . join ( '/' ) } ` ;
490
549
}
491
550
551
+ /**
552
+ * Normalizes a pointer segments.
553
+ *
554
+ * @param segment The segment to normalize.
555
+ *
556
+ * @returns {string } The normalized segment.
557
+ */
558
+ private static normalizeSegment ( segment : string ) : JsonPointerSegment {
559
+ if ( / ^ \d + $ / . test ( segment ) ) {
560
+ return Number . parseInt ( segment , 10 ) ;
561
+ }
562
+
563
+ return segment ;
564
+ }
565
+
492
566
/**
493
567
* Converts a segment to its normalized form.
494
568
*
495
569
* @param segment The escaped segment to convert into its normalized form.
496
570
*/
497
571
private static unescapeSegment ( segment : string ) : JsonPointerSegment {
498
- if ( / ^ \d + $ / . test ( segment ) ) {
499
- return parseInt ( segment , 10 ) ;
572
+ const normalizedSegment = JsonPointer . normalizeSegment ( segment ) ;
573
+
574
+ if ( typeof normalizedSegment === 'number' ) {
575
+ return normalizedSegment ;
500
576
}
501
577
502
578
/*
@@ -506,7 +582,7 @@ export class JsonPointer implements JsonConvertible {
506
582
* which would be incorrect (the string '~01' correctly becomes '~1'
507
583
* after transformation).
508
584
*/
509
- return segment . replace ( / ~ 1 / g, '/' )
585
+ return normalizedSegment . replace ( / ~ 1 / g, '/' )
510
586
. replace ( / ~ 0 / g, '~' ) ;
511
587
}
512
588
0 commit comments