11import { JsonConvertible , JsonStructure , JsonValue } from '@croct/json' ;
22
3+ /**
4+ * A value that can be converted to a JSON pointer.
5+ */
36export type JsonPointerLike = JsonPointer | number | string | JsonPointerSegments ;
47
8+ /**
9+ * A JSON pointer segment.
10+ */
511export type JsonPointerSegment = string | number ;
612
13+ /**
14+ * A list of JSON pointer segments.
15+ */
716export type JsonPointerSegments = JsonPointerSegment [ ] ;
817
918/**
@@ -39,16 +48,32 @@ export class InvalidReferenceError extends JsonPointerError {
3948 }
4049}
4150
51+ /**
52+ * A key-value pair representing a JSON pointer segment and its value.
53+ */
54+ export type Entry = [ JsonPointerSegment | null , JsonValue ] ;
55+
4256/**
4357 * An RFC 6901-compliant JSON pointer.
4458 *
4559 * @see https://tools.ietf.org/html/rfc6901
4660 */
4761export class JsonPointer implements JsonConvertible {
62+ /**
63+ * A singleton representing the root pointer.
64+ */
4865 private static readonly ROOT_SINGLETON = new JsonPointer ( [ ] ) ;
4966
67+ /**
68+ * The list of segments that form the pointer.
69+ */
5070 private readonly segments : JsonPointerSegments ;
5171
72+ /**
73+ * Initializes a new pointer from a list of segments.
74+ *
75+ * @param segments A list of segments.
76+ */
5277 private constructor ( segments : JsonPointerSegments ) {
5378 this . segments = segments ;
5479 }
@@ -72,7 +97,7 @@ export class JsonPointer implements JsonConvertible {
7297 * - Pointers are returned as given
7398 * - Numbers are used as single segments
7499 * - 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
76101 *
77102 * @param path A pointer-like value.
78103 *
@@ -86,7 +111,7 @@ export class JsonPointer implements JsonConvertible {
86111 }
87112
88113 if ( Array . isArray ( path ) ) {
89- return JsonPointer . fromSegments ( path ) ;
114+ return JsonPointer . fromSegments ( path . map ( JsonPointer . normalizeSegment ) ) ;
90115 }
91116
92117 if ( typeof path === 'number' ) {
@@ -99,7 +124,7 @@ export class JsonPointer implements JsonConvertible {
99124 /**
100125 * Creates a pointer from a list of unescaped segments.
101126 *
102- * Numeric segments must be finite non-negative integers.
127+ * Numeric segments must be safe non-negative integers.
103128 *
104129 * @param {JsonPointerSegments } segments A list of unescaped segments.
105130 *
@@ -146,9 +171,9 @@ export class JsonPointer implements JsonConvertible {
146171 }
147172
148173 /**
149- * Checks whether the reference points to an array element.
174+ * Checks whether the pointer references an array element.
150175 *
151- * @returns {boolean } Whether the pointer is an array index.
176+ * @returns {boolean } Whether the pointer references an array index.
152177 */
153178 public isIndex ( ) : boolean {
154179 return typeof this . segments [ this . segments . length - 1 ] === 'number' ;
@@ -161,7 +186,7 @@ export class JsonPointer implements JsonConvertible {
161186 *
162187 * @example
163188 * // returns 2
164- * Pointer .from('/foo/bar').depth()
189+ * JsonPointer .from('/foo/bar').depth()
165190 *
166191 * @returns {number } The depth of the pointer.
167192 */
@@ -225,8 +250,8 @@ export class JsonPointer implements JsonConvertible {
225250 * These are equivalent:
226251 *
227252 * ```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'])
230255 * ```
231256 *
232257 * @param {JsonPointer } other The pointer to append to this one.
@@ -246,78 +271,44 @@ export class JsonPointer implements JsonConvertible {
246271 /**
247272 * Returns the value at the referenced location.
248273 *
249- * @param {JsonStructure } structure The structure to get the value from.
274+ * @param {JsonValue } value The value to read from.
250275 *
251276 * @returns {JsonValue } The value at the referenced location.
252277 *
253278 * @throws {InvalidReferenceError } If a numeric segment references a non-array value.
254279 * @throws {InvalidReferenceError } If a string segment references an array value.
255280 * @throws {InvalidReferenceError } If there is no value at any level of the pointer.
256281 */
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 ) ;
287284
288- continue ;
289- }
285+ let result = iterator . next ( ) ;
290286
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 ( ) ;
296289
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 ;
301292 }
302293
303- current = current [ segment ] ;
294+ result = next ;
304295 }
305296
306- return current ;
297+ return result . value [ 1 ] ;
307298 }
308299
309300 /**
310301 * Checks whether the value at the referenced location exists.
311302 *
312303 * This method gracefully handles missing values by returning `false`.
313304 *
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 .
315306 *
316307 * @returns {JsonValue } Returns `true` if the value exists, `false` otherwise.
317308 */
318- public has ( structure : JsonStructure ) : boolean {
309+ public has ( root : JsonStructure ) : boolean {
319310 try {
320- this . get ( structure ) ;
311+ this . get ( root ) ;
321312 } catch {
322313 return false ;
323314 }
@@ -328,8 +319,8 @@ export class JsonPointer implements JsonConvertible {
328319 /**
329320 * Sets the value at the referenced location.
330321 *
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 .
333324 *
334325 * @throws {InvalidReferenceError } If the pointer references the root of the structure.
335326 * @throws {InvalidReferenceError } If a numeric segment references a non-array value.
@@ -338,12 +329,12 @@ export class JsonPointer implements JsonConvertible {
338329 * @throws {InvalidReferenceError } If setting the value to an array would cause it to become
339330 * sparse.
340331 */
341- public set ( structure : JsonStructure , value : JsonValue ) : void {
332+ public set ( root : JsonStructure , value : JsonValue ) : void {
342333 if ( this . isRoot ( ) ) {
343334 throw new JsonPointerError ( 'Cannot set root value.' ) ;
344335 }
345336
346- const parent = this . getParent ( ) . get ( structure ) ;
337+ const parent = this . getParent ( ) . get ( root ) ;
347338
348339 if ( typeof parent !== 'object' || parent === null ) {
349340 throw new JsonPointerError ( `Cannot set value at "${ this . getParent ( ) } ".` ) ;
@@ -388,22 +379,22 @@ export class JsonPointer implements JsonConvertible {
388379 * is a no-op. Pointers referencing array elements remove the element while keeping
389380 * the array dense.
390381 *
391- * @param {JsonStructure } structure The structure to unset the value at the referenced location .
382+ * @param {JsonStructure } root The value to write to .
392383 *
393384 * @returns {JsonValue } The unset value, or `undefined` if the referenced location
394385 * does not exist.
395386 *
396- * @throws {InvalidReferenceError } If the pointer references the root of the structure .
387+ * @throws {InvalidReferenceError } If the pointer references the root of the root .
397388 */
398- public unset ( structure : JsonStructure ) : JsonValue | undefined {
389+ public unset ( root : JsonStructure ) : JsonValue | undefined {
399390 if ( this . isRoot ( ) ) {
400391 throw new InvalidReferenceError ( 'Cannot unset the root value.' ) ;
401392 }
402393
403394 let parent : JsonValue ;
404395
405396 try {
406- parent = this . getParent ( ) . get ( structure ) ;
397+ parent = this . getParent ( ) . get ( root ) ;
407398 } catch {
408399 return undefined ;
409400 }
@@ -438,6 +429,74 @@ export class JsonPointer implements JsonConvertible {
438429 return value ;
439430 }
440431
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+
441500 /**
442501 * Checks whether the pointer is logically equivalent to another pointer.
443502 *
@@ -489,14 +548,31 @@ export class JsonPointer implements JsonConvertible {
489548 return `/${ this . segments . map ( JsonPointer . escapeSegment ) . join ( '/' ) } ` ;
490549 }
491550
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+
492566 /**
493567 * Converts a segment to its normalized form.
494568 *
495569 * @param segment The escaped segment to convert into its normalized form.
496570 */
497571 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 ;
500576 }
501577
502578 /*
@@ -506,7 +582,7 @@ export class JsonPointer implements JsonConvertible {
506582 * which would be incorrect (the string '~01' correctly becomes '~1'
507583 * after transformation).
508584 */
509- return segment . replace ( / ~ 1 / g, '/' )
585+ return normalizedSegment . replace ( / ~ 1 / g, '/' )
510586 . replace ( / ~ 0 / g, '~' ) ;
511587 }
512588
0 commit comments