All files / json-pointer/src find.ts

94.59% Statements 35/37
83.33% Branches 15/18
75% Functions 3/4
96.29% Lines 26/27

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89    3x                       3x                                                   3x 12x 12x     10x 22x 22x 22x 5x 5x   4x 4x 4x 4x 4x     4x 17x 16x 1x   8x 8x                   3x 28x   14x               3x    
/* tslint:disable no-string-throw */
 
import {hasOwnProperty as has} from '@jsonjoy.com/util/lib/hasOwnProperty';
import type {Path} from './types';
 
export interface Reference {
  /** Target value where pointer is pointing. */
  readonly val: unknown;
  /** Object which contains the target value. */
  readonly obj?: unknown | object | unknown[];
  /** Key which targets the target value in the object. */
  readonly key?: string | number;
}
 
const {isArray} = Array;
 
/**
 * Finds a target in document specified by JSON Pointer. Also returns the
 * object containing the target and key used to reference that object.
 *
 * Throws Error('NOT_FOUND') if pointer does not result into a value in the middle
 * of the path. If the last element of the path does not result into a value, the
 * lookup succeeds with `val` set to `undefined`. It can be used to discriminate
 * missing values, because `undefined` is not a valid JSON value.
 *
 * If last element in array is targeted using "-", e.g. "/arr/-", use
 * `isArrayEnd` to verify that:
 *
 * ```js
 * const ref = find({arr: [1, 2, 3], ['arr', '-']});
 * if (isArrayReference(ref)) {
 *   if (isArrayEnd(ref)) {
 *     // ...
 *   }
 * }
 * ```
 *
 * @param skipLast Number of steps to skip at the end. Useful to find reference of
 *   parent step, without constructing a new `Path` array.
 */
export const find = (val: unknown, path: Path): Reference => {
  const pathLength = path.length;
  if (!pathLength) return {val};
  let obj: Reference['obj'];
  let key: Reference['key'];
  for (let i = 0; i < pathLength; i++) {
    obj = val;
    key = path[i];
    if (isArray(obj)) {
      const length = obj.length;
      if (key === '-') key = length;
      else {
        if (typeof key === 'string') {
          const key2 = ~~key;
          Iif ('' + key2 !== key) throw new Error('INVALID_INDEX');
          key = key2;
          if (key < 0) throw new Error('INVALID_INDEX');
        }
      }
      val = obj[key];
    } else if (typeof obj === 'object' && !!obj) {
      val = has(obj, key as string) ? (obj as any)[key] : undefined;
    } else throw new Error('NOT_FOUND');
  }
  const ref: Reference = {val, obj, key};
  return ref;
};
 
export interface ArrayReference<T = unknown> {
  /** `undefined` in case JSON Pointer points to last element, e.g. "/foo/-". */
  readonly val: undefined | T;
  readonly obj: T[];
  readonly key: number;
}
 
export const isArrayReference = <T = unknown>(ref: Reference): ref is ArrayReference<T> =>
  isArray(ref.obj) && typeof ref.key === 'number';
 
export const isArrayEnd = (ref: ArrayReference): boolean => ref.obj.length === ref.key;
 
export interface ObjectReference<T = unknown> {
  readonly val: T;
  readonly obj: Record<string, T>;
  readonly key: string;
}
 
export const isObjectReference = <T = unknown>(ref: Reference): ref is ObjectReference<T> =>
  typeof ref.obj === 'object' && typeof ref.key === 'string';