"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.ObjectReader = void 0; const optional_1 = require("../../types/optional"); const clone_1 = require("../../util/clone"); const coercion_1 = require("./coercion"); const key_path_1 = require("./key-path"); const object_cursor_1 = require("./object-cursor"); const traverse_1 = require("./traverse"); /* eslint-disable no-underscore-dangle */ /** * Map which holds any object readers recycled, divided by constructor. */ // eslint-disable-next-line @typescript-eslint/ban-types const scrapReaders = new Map(); /** * A type which allows efficient and type-safe traversal of untyped objects. */ class ObjectReader { /** * Create a reader to traverse the contents of an untyped * object safely and efficiently. * * @param object - An object to efficiently traverse with a reader. */ constructor(object) { this._cursor = new object_cursor_1.ObjectCursor(object); } // endsection // section Structure /** * Current key path which operations on this reader are relative to. */ get currentKeyPath() { return this._cursor.currentKeyPath; } /** * Determines whether a value exists for a given key * relative to the reader's current location. * * @param key - The key to test for the existence of. * @returns `true` if a value exists for `key`; `false` otherwise. */ has(key) { return (0, key_path_1.keyPathEndsWith)(this._cursor.currentKeyPath, key) || (0, optional_1.isSome)(this.get(key)); } /** * Make all operations on this reader be relative to a given key path. * * Consecutive calls to `select` with the same key path are idempotent. * You may repeatedly call this method with the same key path and only * the first call will change what operations are relative to on this reader. * * To allow repeated paths in consecutive `select` calls set the optional * `allowRepeatedKeyPath` argument to `true`. * * You must balance calls to this method with matching calls to `deselect`. * * @param keyPath - The key path to make this reader's operations relative to. * @param allowRepeatedKeyPath - The Boolean indicating whether repeated key path * like 'value.value' should be accepted by the reader. * Some JSON objects can have nested properties stored under the same key path. * @returns The reader this method was called on. */ select(keyPath, allowRepeatedKeyPath = false) { if (allowRepeatedKeyPath || !(0, key_path_1.keyPathsEqual)(this._cursor.currentKeyPath, keyPath)) { this._cursor.moveTo(keyPath); } return this; } /** * Make all operations on this reader be relative to the previously selected key path. * * If no key path was previously selected, this method has the effect of making * operations relative to the media response the reader was created to work on. * * Use this method to balance previous calls to a method in the `select` family. * * @returns The reader this method was called on. */ deselect() { this._cursor.moveBack(); return this; } /** * Save the current selection of this reader so that it can be restored later. * * Calls to this method should be balanced with a call to `restoreSelection`. */ saveSelection() { this._cursor.saveState(); return this; } /** * Restore a previous selection of this reader. * * Use this method to balance a previous call to `saveSelection`. */ restoreSelection() { this._cursor.restoreState(); return this; } // endsection // section Scalars /** * Access an untyped value in this reader's contents. * * @param keyPath - A key path specifying where to find the value in this reader's contents. * @returns An optional untyped value. */ get(keyPath = key_path_1.thisKeyPath) { if ((0, key_path_1.isKeyPathThis)(keyPath)) { return this._cursor.currentValue; } else { return (0, traverse_1.traverse)(this._cursor.currentValue, keyPath); } } /** * Access a boolean value in this reader's contents. * * @param keyPath - A key path specifying where to find the value in this reader's contents. * @returns An optional boolean value. */ asBoolean(keyPath = key_path_1.thisKeyPath, policy = "coercible") { return (0, coercion_1.valueAsBoolean)(this.get(keyPath), policy, String(keyPath)); } /** * Access a number value in this reader's contents. * * @param keyPath - A key path specifying where to find the value in this reader's contents. * @returns An optional number value. */ asNumber(keyPath = key_path_1.thisKeyPath, policy = "coercible") { return (0, coercion_1.valueAsNumber)(this.get(keyPath), policy, String(keyPath)); } /** * Access a string value in this reader's contents. * * @param keyPath - A key path specifying where to find the value in this reader's contents. * @returns An optional string value. */ asString(keyPath = key_path_1.thisKeyPath, policy = "coercible") { return (0, coercion_1.valueAsString)(this.get(keyPath), policy, String(keyPath)); } // endsection // section Sequences /** * Create an iterator for the contents of this reader. * * If the current reader's contents are `undefined` or `null`, * the returned iterator yields nothing. * * If the current reader's contents is an array, the returned * iterator will yield a reader for each element in that array. * * Otherwise, the iterator will yield a single reader for * the current reader's contents. * * __Important:__ The readers yielded by this iterator must not * be allowed to escape your `for`-loop. For efficiency, readers * may be reused. * * An iterator consumer (`for...of` loop) may safely call select * methods on the reader without balancing them with deselect * calls before the getting the next reader from the iterator. */ *[Symbol.iterator]() { const iteratee = this.get(); if ((0, optional_1.isNothing)(iteratee)) { return; } const iterationReader = ObjectReader._clone(this); if (Array.isArray(iteratee)) { let index = 0; for (const value of iteratee) { iterationReader.saveSelection(); iterationReader._cursor.interject(value, index); yield iterationReader; iterationReader.restoreSelection(); index += 1; } } else { yield iterationReader; } ObjectReader._recycle(iterationReader); } /** * Returns the result of combining the contents of this reader * using a given function. * * If the current reader's contents are `undefined` or `null`, * the `initialValue` is returned unchanged. * * If the current reader's contents is an array, the `reducer` * will be called with a reader for each element in that array. * * Otherwise, the `reducer` function will be called once with * a reader for the current reader's contents. * * __Important:__ The `reducer` function must not allow the passed in * reader to escape its body. For efficiency, readers may be reused. * The function may safely perform call select methods without balancing * them with matching deselect calls. * * @param initialValue - The value to use as the initial accumulating value. * @param reducer - A function that combines an accumulating value and an element from this reader's contents * into a new accumulating value, to be used in the next call of this function or returned to the caller. */ reduce(initialValue, reducer) { const iteratee = this.get(); if ((0, optional_1.isNothing)(iteratee)) { return initialValue; } if (Array.isArray(iteratee)) { try { let value = initialValue; for (let index = 0, length = iteratee.length; index < length; index += 1) { this.saveSelection(); this._cursor.interject(iteratee[index], index); value = reducer(value, this); this.restoreSelection(); } return value; } catch (e) { this.restoreSelection(); throw e; } } else { return reducer(initialValue, this); } } /** * Create an array by applying a function to the contents of this reader. * * If the current reader's contents are `undefined` or `null`, * an empty array will be returned without calling `transformer`. * * If the current reader's contents is an array, the function will * be called with a reader for each element from that array. * * Otherwise, the function will be called once with a reader for * the current reader's contents. * * __Important:__ The function must not allow the passed in reader * to escape its body. For efficiency, readers may be reused. * The function may safely perform call select methods without balancing * them with matching deselect calls. * * @param transformer - A function which derives a value from a reader. * @returns An array containing the accumulated results of calling `transformer`. */ map(transformer) { return this.reduce(new Array(), (acc, reader) => { acc.push(transformer(reader)); return acc; }); } /** * Create an array by applying a function to the contents of this reader, * discarding `undefined` and `null` values returned by the function. * * If the current reader's contents are `undefined` or `null`, * an empty array will be returned without calling `transformer`. * * If the current reader's contents is an array, the function will * be called with a reader for each element from that array. * * Otherwise, the function will be called once with a reader for * the current reader's contents. * * __Important:__ The function must not allow the passed in reader * to escape its body. For efficiency, readers may be reused. * The function may safely perform call select methods without balancing * them with matching deselect calls. * * @param transformer - A function which derives a value from a reader, * or returns a nully value if none can be derived. * @returns An array containing the accumulated results of calling `transformer`. */ compactMap(transformer) { return this.reduce(new Array(), (acc, reader) => { const value = transformer(reader); if ((0, optional_1.isSome)(value)) { acc.push(value); } return acc; }); } // endsection // section Builders /** * Call a function with this reader and any number of additional parameters, * rolling back any reader selection changes the function makes. * * Use this method to work with closures and top level functions which use * an object reader to do work. Prefer `#callOn` for object methods. * * @param body - A function which takes a reader and any number of additional parameters. * @param rest - The parameters to pass to `body` after this reader. * @returns The result of `body`, if any. */ applyTo(body, ...rest) { this.saveSelection(); try { const result = body(this, ...rest); this.restoreSelection(); return result; } catch (e) { this.restoreSelection(); throw e; } } /** * Call an object method with this reader and any number of additional parameters, * rolling back any reader selection changes the method makes. * * Use this method to work with object methods which use an object reader to do work. * Prefer `#applyTo` for closures and top level functions. * * @param method - A method which takes a reader and any number of additional parameters. * @param thisArg - The object to be used as the current object. * @param rest - The parameters to pass to `method` after this reader. * @returns The result of `method`, if any. */ callOn(method, thisArg, ...rest) { this.saveSelection(); try { const result = method.call(thisArg, this, ...rest); this.restoreSelection(); return result; } catch (e) { this.restoreSelection(); throw e; } } // endsection // section Cloneable clone() { const copy = (0, clone_1.shallowCloneOf)(this); copy._cursor = this._cursor.clone(); return copy; } // endsection // section Reuse /** * Reduce allocations required when iterating with this object reader * up to a specified depth. * * Each subclass of `ObjectReader` should call this method on itself * after the module containing the subclass is loaded. * * @param depth - The expected iteration depth of this object reader type. */ static optimizeIterationUpToDepth(depth) { for (let index = 0; index < depth; index += 1) { ObjectReader._recycle(new ObjectReader(undefined)); } } /** * Clone a given object reader, reusing a previously created instance * of the same constructor if one is available. * * @param reader - The object reader to efficiently clone. * @returns A new reader which can be treated as clone of `reader`. */ static _clone(reader) { const scrap = scrapReaders.get(reader.constructor); if ((0, optional_1.isSome)(scrap)) { const reclaimedReader = scrap.pop(); if ((0, optional_1.isSome)(reclaimedReader)) { reclaimedReader.onReuseToIterate(reader); return reclaimedReader; } } return reader.clone(); } /** * Informs an object reader it is about to be reused as the value * of another object reader which is being treated as an iterator. * * Subclasses _must_ call `super` when overriding this method. * * @param other - The reader this instance is being used to assist. */ onReuseToIterate(other) { const cursorToMirror = other._cursor; this._cursor.reuse(cursorToMirror.currentValue, cursorToMirror.currentKeyPath); } /** * Recycle an object reader which was used as the value of another * object reader being treated as an iterator. * * @param reader - A reader which was used for iteration and is no longer * needed for that role. */ static _recycle(reader) { const ctor = reader.constructor; const existingScrap = scrapReaders.get(ctor); if ((0, optional_1.isSome)(existingScrap)) { if (existingScrap.length >= 5) { return; } reader.onRecycleForIteration(); existingScrap.push(reader); } else { reader.onRecycleForIteration(); scrapReaders.set(ctor, [reader]); } } /** * Informs an object reader it is being recycled after being used as * the value of another object reader which was treated as an iterator. * * Subclasses _must_ call `super` when overriding this method. */ onRecycleForIteration() { this._cursor.reuse(undefined); } } exports.ObjectReader = ObjectReader; //# sourceMappingURL=object-reader.js.map