forked from off-topic/apps.apple.com
428 lines
16 KiB
JavaScript
428 lines
16 KiB
JavaScript
"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
|