this repo has no description
1"use strict";
2Object.defineProperty(exports, "__esModule", { value: true });
3exports.ObjectReader = void 0;
4const optional_1 = require("../../types/optional");
5const clone_1 = require("../../util/clone");
6const coercion_1 = require("./coercion");
7const key_path_1 = require("./key-path");
8const object_cursor_1 = require("./object-cursor");
9const traverse_1 = require("./traverse");
10/* eslint-disable no-underscore-dangle */
11/**
12 * Map which holds any object readers recycled, divided by constructor.
13 */
14// eslint-disable-next-line @typescript-eslint/ban-types
15const scrapReaders = new Map();
16/**
17 * A type which allows efficient and type-safe traversal of untyped objects.
18 */
19class ObjectReader {
20 /**
21 * Create a reader to traverse the contents of an untyped
22 * object safely and efficiently.
23 *
24 * @param object - An object to efficiently traverse with a reader.
25 */
26 constructor(object) {
27 this._cursor = new object_cursor_1.ObjectCursor(object);
28 }
29 // endsection
30 // section Structure
31 /**
32 * Current key path which operations on this reader are relative to.
33 */
34 get currentKeyPath() {
35 return this._cursor.currentKeyPath;
36 }
37 /**
38 * Determines whether a value exists for a given key
39 * relative to the reader's current location.
40 *
41 * @param key - The key to test for the existence of.
42 * @returns `true` if a value exists for `key`; `false` otherwise.
43 */
44 has(key) {
45 return (0, key_path_1.keyPathEndsWith)(this._cursor.currentKeyPath, key) || (0, optional_1.isSome)(this.get(key));
46 }
47 /**
48 * Make all operations on this reader be relative to a given key path.
49 *
50 * Consecutive calls to `select` with the same key path are idempotent.
51 * You may repeatedly call this method with the same key path and only
52 * the first call will change what operations are relative to on this reader.
53 *
54 * To allow repeated paths in consecutive `select` calls set the optional
55 * `allowRepeatedKeyPath` argument to `true`.
56 *
57 * You must balance calls to this method with matching calls to `deselect`.
58 *
59 * @param keyPath - The key path to make this reader's operations relative to.
60 * @param allowRepeatedKeyPath - The Boolean indicating whether repeated key path
61 * like 'value.value' should be accepted by the reader.
62 * Some JSON objects can have nested properties stored under the same key path.
63 * @returns The reader this method was called on.
64 */
65 select(keyPath, allowRepeatedKeyPath = false) {
66 if (allowRepeatedKeyPath || !(0, key_path_1.keyPathsEqual)(this._cursor.currentKeyPath, keyPath)) {
67 this._cursor.moveTo(keyPath);
68 }
69 return this;
70 }
71 /**
72 * Make all operations on this reader be relative to the previously selected key path.
73 *
74 * If no key path was previously selected, this method has the effect of making
75 * operations relative to the media response the reader was created to work on.
76 *
77 * Use this method to balance previous calls to a method in the `select` family.
78 *
79 * @returns The reader this method was called on.
80 */
81 deselect() {
82 this._cursor.moveBack();
83 return this;
84 }
85 /**
86 * Save the current selection of this reader so that it can be restored later.
87 *
88 * Calls to this method should be balanced with a call to `restoreSelection`.
89 */
90 saveSelection() {
91 this._cursor.saveState();
92 return this;
93 }
94 /**
95 * Restore a previous selection of this reader.
96 *
97 * Use this method to balance a previous call to `saveSelection`.
98 */
99 restoreSelection() {
100 this._cursor.restoreState();
101 return this;
102 }
103 // endsection
104 // section Scalars
105 /**
106 * Access an untyped value in this reader's contents.
107 *
108 * @param keyPath - A key path specifying where to find the value in this reader's contents.
109 * @returns An optional untyped value.
110 */
111 get(keyPath = key_path_1.thisKeyPath) {
112 if ((0, key_path_1.isKeyPathThis)(keyPath)) {
113 return this._cursor.currentValue;
114 }
115 else {
116 return (0, traverse_1.traverse)(this._cursor.currentValue, keyPath);
117 }
118 }
119 /**
120 * Access a boolean value in this reader's contents.
121 *
122 * @param keyPath - A key path specifying where to find the value in this reader's contents.
123 * @returns An optional boolean value.
124 */
125 asBoolean(keyPath = key_path_1.thisKeyPath, policy = "coercible") {
126 return (0, coercion_1.valueAsBoolean)(this.get(keyPath), policy, String(keyPath));
127 }
128 /**
129 * Access a number value in this reader's contents.
130 *
131 * @param keyPath - A key path specifying where to find the value in this reader's contents.
132 * @returns An optional number value.
133 */
134 asNumber(keyPath = key_path_1.thisKeyPath, policy = "coercible") {
135 return (0, coercion_1.valueAsNumber)(this.get(keyPath), policy, String(keyPath));
136 }
137 /**
138 * Access a string value in this reader's contents.
139 *
140 * @param keyPath - A key path specifying where to find the value in this reader's contents.
141 * @returns An optional string value.
142 */
143 asString(keyPath = key_path_1.thisKeyPath, policy = "coercible") {
144 return (0, coercion_1.valueAsString)(this.get(keyPath), policy, String(keyPath));
145 }
146 // endsection
147 // section Sequences
148 /**
149 * Create an iterator for the contents of this reader.
150 *
151 * If the current reader's contents are `undefined` or `null`,
152 * the returned iterator yields nothing.
153 *
154 * If the current reader's contents is an array, the returned
155 * iterator will yield a reader for each element in that array.
156 *
157 * Otherwise, the iterator will yield a single reader for
158 * the current reader's contents.
159 *
160 * __Important:__ The readers yielded by this iterator must not
161 * be allowed to escape your `for`-loop. For efficiency, readers
162 * may be reused.
163 *
164 * An iterator consumer (`for...of` loop) may safely call select
165 * methods on the reader without balancing them with deselect
166 * calls before the getting the next reader from the iterator.
167 */
168 *[Symbol.iterator]() {
169 const iteratee = this.get();
170 if ((0, optional_1.isNothing)(iteratee)) {
171 return;
172 }
173 const iterationReader = ObjectReader._clone(this);
174 if (Array.isArray(iteratee)) {
175 let index = 0;
176 for (const value of iteratee) {
177 iterationReader.saveSelection();
178 iterationReader._cursor.interject(value, index);
179 yield iterationReader;
180 iterationReader.restoreSelection();
181 index += 1;
182 }
183 }
184 else {
185 yield iterationReader;
186 }
187 ObjectReader._recycle(iterationReader);
188 }
189 /**
190 * Returns the result of combining the contents of this reader
191 * using a given function.
192 *
193 * If the current reader's contents are `undefined` or `null`,
194 * the `initialValue` is returned unchanged.
195 *
196 * If the current reader's contents is an array, the `reducer`
197 * will be called with a reader for each element in that array.
198 *
199 * Otherwise, the `reducer` function will be called once with
200 * a reader for the current reader's contents.
201 *
202 * __Important:__ The `reducer` function must not allow the passed in
203 * reader to escape its body. For efficiency, readers may be reused.
204 * The function may safely perform call select methods without balancing
205 * them with matching deselect calls.
206 *
207 * @param initialValue - The value to use as the initial accumulating value.
208 * @param reducer - A function that combines an accumulating value and an element from this reader's contents
209 * into a new accumulating value, to be used in the next call of this function or returned to the caller.
210 */
211 reduce(initialValue, reducer) {
212 const iteratee = this.get();
213 if ((0, optional_1.isNothing)(iteratee)) {
214 return initialValue;
215 }
216 if (Array.isArray(iteratee)) {
217 try {
218 let value = initialValue;
219 for (let index = 0, length = iteratee.length; index < length; index += 1) {
220 this.saveSelection();
221 this._cursor.interject(iteratee[index], index);
222 value = reducer(value, this);
223 this.restoreSelection();
224 }
225 return value;
226 }
227 catch (e) {
228 this.restoreSelection();
229 throw e;
230 }
231 }
232 else {
233 return reducer(initialValue, this);
234 }
235 }
236 /**
237 * Create an array by applying a function to the contents of this reader.
238 *
239 * If the current reader's contents are `undefined` or `null`,
240 * an empty array will be returned without calling `transformer`.
241 *
242 * If the current reader's contents is an array, the function will
243 * be called with a reader for each element from that array.
244 *
245 * Otherwise, the function will be called once with a reader for
246 * the current reader's contents.
247 *
248 * __Important:__ The function must not allow the passed in reader
249 * to escape its body. For efficiency, readers may be reused.
250 * The function may safely perform call select methods without balancing
251 * them with matching deselect calls.
252 *
253 * @param transformer - A function which derives a value from a reader.
254 * @returns An array containing the accumulated results of calling `transformer`.
255 */
256 map(transformer) {
257 return this.reduce(new Array(), (acc, reader) => {
258 acc.push(transformer(reader));
259 return acc;
260 });
261 }
262 /**
263 * Create an array by applying a function to the contents of this reader,
264 * discarding `undefined` and `null` values returned by the function.
265 *
266 * If the current reader's contents are `undefined` or `null`,
267 * an empty array will be returned without calling `transformer`.
268 *
269 * If the current reader's contents is an array, the function will
270 * be called with a reader for each element from that array.
271 *
272 * Otherwise, the function will be called once with a reader for
273 * the current reader's contents.
274 *
275 * __Important:__ The function must not allow the passed in reader
276 * to escape its body. For efficiency, readers may be reused.
277 * The function may safely perform call select methods without balancing
278 * them with matching deselect calls.
279 *
280 * @param transformer - A function which derives a value from a reader,
281 * or returns a nully value if none can be derived.
282 * @returns An array containing the accumulated results of calling `transformer`.
283 */
284 compactMap(transformer) {
285 return this.reduce(new Array(), (acc, reader) => {
286 const value = transformer(reader);
287 if ((0, optional_1.isSome)(value)) {
288 acc.push(value);
289 }
290 return acc;
291 });
292 }
293 // endsection
294 // section Builders
295 /**
296 * Call a function with this reader and any number of additional parameters,
297 * rolling back any reader selection changes the function makes.
298 *
299 * Use this method to work with closures and top level functions which use
300 * an object reader to do work. Prefer `#callOn` for object methods.
301 *
302 * @param body - A function which takes a reader and any number of additional parameters.
303 * @param rest - The parameters to pass to `body` after this reader.
304 * @returns The result of `body`, if any.
305 */
306 applyTo(body, ...rest) {
307 this.saveSelection();
308 try {
309 const result = body(this, ...rest);
310 this.restoreSelection();
311 return result;
312 }
313 catch (e) {
314 this.restoreSelection();
315 throw e;
316 }
317 }
318 /**
319 * Call an object method with this reader and any number of additional parameters,
320 * rolling back any reader selection changes the method makes.
321 *
322 * Use this method to work with object methods which use an object reader to do work.
323 * Prefer `#applyTo` for closures and top level functions.
324 *
325 * @param method - A method which takes a reader and any number of additional parameters.
326 * @param thisArg - The object to be used as the current object.
327 * @param rest - The parameters to pass to `method` after this reader.
328 * @returns The result of `method`, if any.
329 */
330 callOn(method, thisArg, ...rest) {
331 this.saveSelection();
332 try {
333 const result = method.call(thisArg, this, ...rest);
334 this.restoreSelection();
335 return result;
336 }
337 catch (e) {
338 this.restoreSelection();
339 throw e;
340 }
341 }
342 // endsection
343 // section Cloneable
344 clone() {
345 const copy = (0, clone_1.shallowCloneOf)(this);
346 copy._cursor = this._cursor.clone();
347 return copy;
348 }
349 // endsection
350 // section Reuse
351 /**
352 * Reduce allocations required when iterating with this object reader
353 * up to a specified depth.
354 *
355 * Each subclass of `ObjectReader` should call this method on itself
356 * after the module containing the subclass is loaded.
357 *
358 * @param depth - The expected iteration depth of this object reader type.
359 */
360 static optimizeIterationUpToDepth(depth) {
361 for (let index = 0; index < depth; index += 1) {
362 ObjectReader._recycle(new ObjectReader(undefined));
363 }
364 }
365 /**
366 * Clone a given object reader, reusing a previously created instance
367 * of the same constructor if one is available.
368 *
369 * @param reader - The object reader to efficiently clone.
370 * @returns A new reader which can be treated as clone of `reader`.
371 */
372 static _clone(reader) {
373 const scrap = scrapReaders.get(reader.constructor);
374 if ((0, optional_1.isSome)(scrap)) {
375 const reclaimedReader = scrap.pop();
376 if ((0, optional_1.isSome)(reclaimedReader)) {
377 reclaimedReader.onReuseToIterate(reader);
378 return reclaimedReader;
379 }
380 }
381 return reader.clone();
382 }
383 /**
384 * Informs an object reader it is about to be reused as the value
385 * of another object reader which is being treated as an iterator.
386 *
387 * Subclasses _must_ call `super` when overriding this method.
388 *
389 * @param other - The reader this instance is being used to assist.
390 */
391 onReuseToIterate(other) {
392 const cursorToMirror = other._cursor;
393 this._cursor.reuse(cursorToMirror.currentValue, cursorToMirror.currentKeyPath);
394 }
395 /**
396 * Recycle an object reader which was used as the value of another
397 * object reader being treated as an iterator.
398 *
399 * @param reader - A reader which was used for iteration and is no longer
400 * needed for that role.
401 */
402 static _recycle(reader) {
403 const ctor = reader.constructor;
404 const existingScrap = scrapReaders.get(ctor);
405 if ((0, optional_1.isSome)(existingScrap)) {
406 if (existingScrap.length >= 5) {
407 return;
408 }
409 reader.onRecycleForIteration();
410 existingScrap.push(reader);
411 }
412 else {
413 reader.onRecycleForIteration();
414 scrapReaders.set(ctor, [reader]);
415 }
416 }
417 /**
418 * Informs an object reader it is being recycled after being used as
419 * the value of another object reader which was treated as an iterator.
420 *
421 * Subclasses _must_ call `super` when overriding this method.
422 */
423 onRecycleForIteration() {
424 this._cursor.reuse(undefined);
425 }
426}
427exports.ObjectReader = ObjectReader;
428//# sourceMappingURL=object-reader.js.map