diff --git a/src/pointer.ts b/src/pointer.ts index 0610984..5ce869a 100644 --- a/src/pointer.ts +++ b/src/pointer.ts @@ -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. @@ -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 | any[]; + +export type RootValue = any; + +/** + * A union of all possible values in a structure. + */ +export type ReferencedValue = NestedValue; + +/** + * A union of all possible values in a structure, excluding the given type. + */ +type NestedValue = T | ( + T extends object + ? T extends U + ? NestedValue, U> + : T extends Array + ? NestedValue + : NestedValue + : never + ); + +type Diff = M extends infer U + ? T extends U + ? Exclude extends never + ? never + : Pick> + : never + : never; + /** * An error indicating a problem related to JSON pointer operations. */ @@ -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 = [JsonPointerSegment | null, T]; /** * An RFC 6901-compliant JSON pointer. @@ -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(value: T): ReferencedValue { const iterator = this.traverse(value); let result = iterator.next(); @@ -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 { @@ -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. @@ -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(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]; @@ -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(root: T): ReferencedValue | undefined { if (this.isRoot()) { throw new InvalidReferenceError('Cannot unset the root value.'); } - let parent: JsonValue; + let target: ReferencedValue; 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]; @@ -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} An iterator over the stack of values that the + * @returns {Iterator>} 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 { - let current: JsonValue = root; + public* traverse(root: T): Iterator>> { + let current: ReferencedValue = root; yield [null, current]; @@ -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; yield [segment, current]; } @@ -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; } diff --git a/src/relativePointer.ts b/src/relativePointer.ts index 07fede3..b41878b 100644 --- a/src/relativePointer.ts +++ b/src/relativePointer.ts @@ -1,4 +1,4 @@ -import {JsonConvertible, JsonStructure, JsonValue} from '@croct/json'; +import {JsonConvertible, JsonStructure} from '@croct/json'; import { JsonPointer, JsonPointerSegments, @@ -8,6 +8,8 @@ import { JsonPointerLike, Entry, InvalidReferenceError, + ReferencedValue, + RootValue, } from './pointer'; /** @@ -245,10 +247,10 @@ 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. @@ -256,7 +258,7 @@ export class JsonRelativePointer implements JsonConvertible { * @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(root: T, pointer = JsonPointer.root()): ReferencedValue|JsonPointerSegment { const stack = this.getReferenceStack(root, pointer); const [segment, value] = stack[stack.length - 1]; @@ -268,7 +270,8 @@ export class JsonRelativePointer implements JsonConvertible { return segment; } - return this.getRemainderPointer().get(value); + // Given V = typeof value, and typeof value ⊆ ReferencedValue → ReferencedValue ⊆ ReferencedValue + return this.getRemainderPointer().get(value) as ReferencedValue; } /** @@ -276,12 +279,12 @@ export class JsonRelativePointer implements JsonConvertible { * * 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 { @@ -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. @@ -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.'); } @@ -337,7 +340,7 @@ 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 @@ -345,7 +348,7 @@ export class JsonRelativePointer implements JsonConvertible { * * @throws {InvalidReferenceError} If the pointer references the root of the structure. */ - public unset(root: JsonValue, pointer = JsonPointer.root()): JsonValue | undefined { + public unset(root: T, pointer = JsonPointer.root()): ReferencedValue | undefined { if (this.isKeyPointer()) { throw new JsonPointerError('Cannot write to a key.'); } @@ -354,7 +357,8 @@ 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 → ReferencedValue ⊆ ReferencedValue + return remainderPointer.unset(stack[stack.length - 1][1]) as ReferencedValue; } if (stack.length < 2) { @@ -362,28 +366,29 @@ export class JsonRelativePointer implements JsonConvertible { } 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 → ReferencedValue ⊆ ReferencedValue + return JsonPointer.from([segment]).unset(parent) as ReferencedValue; } /** * 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[]} 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(root: T, pointer: JsonPointer): Array>> { const iterator = pointer.traverse(root); let current = iterator.next(); - const stack: Entry[] = []; + const stack: Array>> = []; while (current.done === false) { stack.push(current.value); @@ -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; } diff --git a/test/pointer.test.ts b/test/pointer.test.ts index 50fa04d..8bb6762 100644 --- a/test/pointer.test.ts +++ b/test/pointer.test.ts @@ -1,4 +1,4 @@ -import {JsonStructure, JsonValue} from '@croct/json'; +import {JsonValue} from '@croct/json'; import { Entry, InvalidReferenceError, @@ -7,6 +7,7 @@ import { JsonPointerError, JsonPointerLike, JsonPointerSegments, + RootValue, } from '../src'; describe('A JSON Pointer', () => { @@ -212,6 +213,11 @@ describe('A JSON Pointer', () => { {foo: 'bar'}, 'bar', ], + [ + JsonPointer.parse('/foo'), + {foo: undefined}, + undefined, + ], [ JsonPointer.root(), ['foo'], @@ -275,8 +281,8 @@ describe('A JSON Pointer', () => { ], )( 'should get value at "%s" from %s', - (pointer: JsonPointer, structure: JsonStructure, value: JsonValue) => { - expect(pointer.get(structure)).toStrictEqual(value); + (pointer: JsonPointer, root: RootValue, value: RootValue) => { + expect(pointer.get(root)).toStrictEqual(value); }, ); @@ -287,11 +293,6 @@ describe('A JSON Pointer', () => { {bar: 'foo'}, 'Property "foo" does not exist at "".', ], - [ - JsonPointer.parse('/foo'), - {foo: undefined}, - 'Property "foo" does not exist at "".', - ], [ JsonPointer.parse('/foo'), [], @@ -335,8 +336,8 @@ describe('A JSON Pointer', () => { ], )( 'should fail to get value at "%s" from %s because "%s"', - (pointer: JsonPointer, structure: JsonStructure, expectedError: string) => { - expect(() => pointer.get(structure)).toThrowWithMessage( + (pointer: JsonPointer, root: RootValue, expectedError: string) => { + expect(() => pointer.get(root)).toThrowWithMessage( InvalidReferenceError, expectedError, ); @@ -355,6 +356,11 @@ describe('A JSON Pointer', () => { {foo: 'bar'}, true, ], + [ + JsonPointer.parse('/foo'), + {foo: undefined}, + true, + ], [ JsonPointer.parse('/bar'), {foo: 'bar'}, @@ -438,8 +444,8 @@ describe('A JSON Pointer', () => { ], )( 'should report the existence of a value at "%s" in %s as %s', - (pointer: JsonPointer, structure: JsonStructure, result: boolean) => { - expect(pointer.has(structure)).toStrictEqual(result); + (pointer: JsonPointer, root: RootValue, result: boolean) => { + expect(pointer.has(root)).toStrictEqual(result); }, ); @@ -451,6 +457,12 @@ describe('A JSON Pointer', () => { 'baz', {bar: 'baz'}, ], + [ + JsonPointer.parse('/bar'), + {bar: 'foo'}, + undefined, + {bar: undefined}, + ], [ JsonPointer.parse('/foo'), {bar: 'foo'}, @@ -537,13 +549,13 @@ describe('A JSON Pointer', () => { 'should set value at "%s" into %s', ( pointer: JsonPointer, - structure: JsonStructure, - value: JsonValue, - expectedResult: JsonStructure, + root: RootValue, + value: RootValue, + expectedResult: RootValue, ) => { - pointer.set(structure, value); + pointer.set(root, value); - expect(structure).toStrictEqual(expectedResult); + expect(root).toStrictEqual(expectedResult); }, ); @@ -562,8 +574,8 @@ describe('A JSON Pointer', () => { ], )( 'should fail to set value at "%s" into %s because "%s"', - (pointer: JsonPointer, structure: JsonStructure, errorMessage: string) => { - expect(() => pointer.set(structure, null)).toThrowWithMessage( + (pointer: JsonPointer, root: RootValue, errorMessage: string) => { + expect(() => pointer.set(root, null)).toThrowWithMessage( Error, errorMessage, ); @@ -595,8 +607,8 @@ describe('A JSON Pointer', () => { ], )( 'should fail to set value at "%s" into %s with invalid reference error because "%s"', - (pointer: JsonPointer, structure: JsonStructure, errorMessage: string) => { - expect(() => pointer.set(structure, null)).toThrowWithMessage( + (pointer: JsonPointer, root: RootValue, errorMessage: string) => { + expect(() => pointer.set(root, null)).toThrowWithMessage( InvalidReferenceError, errorMessage, ); @@ -633,8 +645,8 @@ describe('A JSON Pointer', () => { ], )( 'should fail to set value at "%s" into %s with json pointer error because "%s"', - (pointer: JsonPointer, structure: JsonStructure, errorMessage: string) => { - expect(() => pointer.set(structure, null)).toThrowWithMessage( + (pointer: JsonPointer, root: RootValue, errorMessage: string) => { + expect(() => pointer.set(root, null)).toThrowWithMessage( JsonPointerError, errorMessage, ); @@ -789,12 +801,12 @@ describe('A JSON Pointer', () => { 'should unset value at "%s" from %s, removing %s and resulting in %s', ( pointer: JsonPointer, - structure: JsonStructure, + root: RootValue, unsetValue: JsonValue|undefined, expectedResult: JsonValue, ) => { - expect(pointer.unset(structure)).toStrictEqual(unsetValue); - expect(structure).toStrictEqual(expectedResult); + expect(pointer.unset(root)).toStrictEqual(unsetValue); + expect(root).toStrictEqual(expectedResult); }, ); @@ -805,7 +817,7 @@ describe('A JSON Pointer', () => { ); }); - it.each<[JsonPointer, JsonStructure, Entry[]]>( + it.each<[JsonPointer, RootValue, Array>]>( [ [ JsonPointer.root(), @@ -880,8 +892,8 @@ describe('A JSON Pointer', () => { ], )( 'should traverse "%s" from %o and return %o', - (pointer: JsonPointer, structure: JsonStructure, entries: Entry[]) => { - expect(toArray(pointer.traverse(structure))).toStrictEqual(entries); + (pointer: JsonPointer, root: RootValue, entries: Array>) => { + expect(toArray(pointer.traverse(root))).toStrictEqual(entries); }, ); @@ -935,8 +947,8 @@ describe('A JSON Pointer', () => { ], )( 'should fail to traverse "%s" from %o because "%s"', - (pointer: JsonPointer, structure: JsonStructure, expectedError: string) => { - expect(() => toArray(pointer.traverse(structure))).toThrowWithMessage(InvalidReferenceError, expectedError); + (pointer: JsonPointer, root: RootValue, expectedError: string) => { + expect(() => toArray(pointer.traverse(root))).toThrowWithMessage(InvalidReferenceError, expectedError); }, ); diff --git a/test/relativePointer.test.ts b/test/relativePointer.test.ts index a6013b5..fba5028 100644 --- a/test/relativePointer.test.ts +++ b/test/relativePointer.test.ts @@ -6,6 +6,7 @@ import { JsonPointerError, JsonPointerLike, JsonPointerSegments, + RootValue, } from '../src'; import {JsonRelativePointer, JsonRelativePointerLike} from '../src/relativePointer'; @@ -451,7 +452,7 @@ describe('A JSON Relative Pointer', () => { )( 'should get value from %o starting from %s with pointer %s resulting in %s', ( - root: JsonValue, + root: RootValue, basePointer: JsonPointer, relativePointer: JsonRelativePointer, result: JsonValue, @@ -945,7 +946,7 @@ describe('A JSON Relative Pointer', () => { )( 'should unset from %o at %s %s resulting in %o', ( - root: JsonValue, + root: RootValue, basePointer: JsonPointer, relativePointer: JsonRelativePointer, result: JsonValue, @@ -984,7 +985,7 @@ describe('A JSON Relative Pointer', () => { )( 'should fail to unset from %o at %s %s because %S', ( - root: JsonValue, + root: RootValue, basePointer: JsonPointer, relativePointer: JsonRelativePointer, error: string,