this repo has no description
at main 16 kB view raw
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