summaryrefslogtreecommitdiff
path: root/node_modules/@jet/environment/json/reader/object-reader.js
blob: f31a222079fd951fe3970c41f73a206fbb203a9d (plain)
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
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
"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