index.js

const { mapObject } = require('underscore');
const { at_maybe, cloneImpl, isLensClass } = require('./src-cjs/constants');
const Errors = require('./cjs/errors');
const fusion = require('./cjs/fusion').default;
const Lens = require('./cjs/lens').default;
const { set: setLogger, enableAsync: asyncLogging } = require('./cjs/logger');
const { eachFound, maybeDo } = require('./cjs/utils');

let fuse = null;

/**
 * @template T
 * @callback module:natural-lenses~BlockLogger
 * @summary Temporarily log to a different logger during callback
 * @see `forBlock` method of [setLogger]{@link module:natural-lenses#setLogger}
 * @since 2.2.0
 * @param {Logger} logger       Custom logger to use, with interface like `console`
 * @param {function(): T} body  Callback to execute, logging to *logger*; receives no arguments and returns T
 * @returns {T}  Return value from *body*
 *
 * @description
 * Calling this method temporarily redirects logging output to *logger* during
 * the execution of *body*.
 *
 * ## Asynchronicity without [asyncLogging]{@link module:natural-lenses#asyncLogging}
 *
 * If *body* registers callbacks that complete after it returns, logging calls
 * will return to targeting the *status quo ante* logger (or whatever logger
 * happens to be current at the time).  There is no way for this library to
 * detect this has happend and warn about pending callbacks logging to a
 * different logger.
 * 
 * If *body* returns a *thenable* (i.e. an object with a `then` property, like
 * a Promise), *logger* will remain the target logger until the result fulfills
 * or rejects.  A warning will also be logged because the target logger for
 * the entire process is changed by this usage.
 *
 * ## Asynchronicity with [asyncLogging]{@link module:natural-lenses#asyncLogging}
 *
 * If [asyncLogging]{@link module:natural-lenses#asyncLogging} has been called
 * to install an asynchronous-context-aware engine, then *logger* will be the
 * target logger during the execution of *body* and *for any asynchronous
 * execution starting within body*, whether via callback or thenable.
 */

/**
 * @module natural-lenses
 * @summary Construct a Lens from the given indexing steps
 *
 * @param {...*} key  A name or index to use in successive subscripting (i.e. square bracket) operations
 * @returns {Lens}  The constructed lens
 *
 * @property {Function} asyncLogging        [Documentation]{@link module:natural-lenses#asyncLogging}
 * @property {symbol}   at_maybe            Key for method implementing retrieval from a container
 * @property {symbol}   clone               Key for method implementing cloning of a container with modifications
 * @property {Function} eachFound           [Documentation]{@link module:natural-lenses#eachFound}
 * @property {Function} Factory             [Class]{@link Factory} for customized lens creation
 * @property {Function} fuse                [Documentation]{@link module:natural-lenses#fuse}
 * @property {symbol}   isLens              Key for testing objects for "lens-ness"
 * @property {Function} JsContainerFactory  [Class]{@link JsContainerFactory} for customized container creation
 * @property {Object}   jsContainers        {@link JsContainerFactory} for standard JavaScript containers (Map and Array)
 * @property {Function} maybeDo             [Documentation]{@link module:natural-lenses#maybeDo}
 * @property {Function} nfocal              [Construct]{@link module:natural-lenses#nfocal} a multifocal lens
 * @property {Function} polyfillImmutable   [Documentation]{@link module:natural-lenses#polyfillImmutable}
 * @property {Function} setLogger           [Documentation]{@link module:natural-lenses#setLogger}
 * @property {Function} Step                [Class]{@link Step} for customized Lens steps
 *
 * @description
 * This module is (when `require`d) or exports as default (when `import`ed) a
 * Function accepting an arbitrary number of keys and returning a {@link Lens}.
 *
 * A Lens is a way to safely apply the indexing (i.e. square-bracket) operator
 * repeatedly, and to clone-with-changes a complex value to assist programming
 * in an immutable-data style.
 *
 * In addition to the properties enumerated here, all error classes are also
 * exposed as properties or named exports.
 */
function makeLens(...keys) {
  return new Lens(...keys);
}
Object.defineProperties(makeLens, {
  at_maybe: {enumerable: true, value: at_maybe},
  clone: {enumerable: true, value: cloneImpl},
  eachFound: {enumerable: true, value: eachFound},
  isLens: {enumerable: true, value: isLensClass},
  maybeDo: {enumerable: true, value: maybeDo},
  ...mapObject(Errors, (cls) => ({enumerable: true, value: cls})),
  
  /**
   * @function module:natural-lenses#fuse
   * @summary Fuse multiple optics into a single, sequential application
   * @param {...Optic} optic  Optic object to fuse
   * @returns {Lens|OpticArray}  A single {@link Optic} joining the *optics*
   *
   * @description
   * To understand the slot reference of the returned optic, consider the
   * *getting* model: `optics[0]` will be applied to the input data, and
   * for all other optics (i > 0), `optics[i]` will be applied to the
   * result from `optics[i - 1]`.  In other words, each optic *gets* something
   * within the optic to its left, with the leftmost *getting* from the
   * input data.  Modifications affect the same slot.
   *
   * If all *optics* are [Lenses]{@link Lens}, the result will be a Lens.  This
   * does not apply for Lens-derived objects (e.g. from [Factories]{@link Factory}) —
   * if such are passed, the result will always be an OpticArray.
   */
  fuse: {enumerable: true, get: () => {
    const OpticArray = require('./cjs/optic_array.js').default;
    fuse = fuse || fusion({ Lens, OpticArray });
    return fuse;
  }},
  
  /**
   * @function module:natural-lenses#nfocal
   * @summary Construct a multifocal lens
   * @param {Array.<Optic> | Object.<string,Optic>} lenses  Collection of [Lenses]{@link Lens} to combine
   * @returns {ArrayNFocal|ObjectNFocal}
   *
   * @description
   * Where a standard lens looks at a single *slot* within a JSONic object, a
   * multifocal lens consists of multiple lenses (standard or multifocal) whose
   * results are aggregated into a single value, which can be a dictionary-like
   * Object or an Array.
   *
   * Pass an Array of lenses to create a multifocal lens that outputs an Array,
   * where the position of each lens in the input Array corresponds to the
   * output position of the data (when *getting* with the multifocal lens).  When
   * *getting* in a Maybe context (`#get_maybe`), the result will always be an
   * Object with a `just` property, though some elements of the Array may be
   * empty.
   *
   * Pass an Object with lens values to create a multifocal lens outputting an
   * Object, which will bear the same properties as the input Object and whose
   * values are the values in the data referenced by the corresponding lens (when
   * *getting* using this multifocal lens).
   *
   * Be aware that the `xformInClone` method on these kinds of multifocal lenses
   * works differently from a basic Lens, since multifocals can have a
   * "stereoscopic" view of data.  Instead of a single transformation function,
   * `xformInClone` (and it's `_maybe` variant) accept an iterable of key/transform
   * pairs (where the key is an integer index in the case of an Array multifocal).
   * This allows full control over the order in which transformations are applied
   * to the input data, resolving the issue of "stereoscopic conflict".
   */
  nfocal: {enumerable: true, get: () => (lenses) => {
    return require('./cjs/nfocal').makeNFocal(lenses);
  }},
  
  /**
   * @function module:natural-lenses#setLogger
   * @summary Set a custom logger
   * @since 2.2.0
   * @param {Logger} logger  Custom logger to use, with interface like `console`
   * @returns {Logger}  The previous logger
   *
   * @property {module:natural-lenses~BlockLogger} forBlock  Set the logger only for the duration of the callback
   *
   * @description
   * Calling this function changes the "current logger"...in some regard.  If
   * [asyncLogging]{@link module:natural-lenses#asyncLogging} has not been
   * called, then the global current logger is changed; if it has been called,
   * the specific engine invoked determines what is set.  In the case of the
   * `node` engine, *logger* becomes the receiver for subsequent logging calls
   * in the synchronous context and any asynchronous contexts subsequently
   * spawned from it.  When the `node` engine is in use and this function is
   * called from an asynchronous context, the logging receiver change is local
   * to that context but propagates to any asynchronous contexts it spawns.
   */
  setLogger: {enumerable: true, get: () => setLogger},
  
  /**
   * @function module:natural-lenses#asyncLogging
   * @summary Enable asynchronous-context awareness for logger assignment
   * @since 2.2.0
   * @param {string} engine  Name of the engine to use
   * @returns The module (for chainable configuration)
   *
   * @description
   * Logging configuration is usually a global activity.  However, there are
   * cases where some specific section of the code needs to redirect its
   * logging to a different handler.
   *
   * In many languages and environments, this diversion of logging events would
   * be handled with a thread- or fiber-local variable.  JavaScript — with its
   * extensive use of closures and asynchronous callbacks/Promises — needs to
   * associate the logging diversion with the asynchronous context of the
   * code setting the logger.  There is, however, no standard way to do this.
   *
   * To bridge this gap, [natural-lenses]{@link module:natural-lenses} provides
   * *asynchronous context engines* which can be configured on demand.  Setting
   * the engine does not change the current logger, but can allow code to
   * change the logger in a more local fashion.  Configuring the appropriate
   * asynchronous context engine makes [setLogger.forBlock]{@link module:natural-lenses~BlockLogger}
   * much more adept at getting logging events to the correct handler in
   * asynchronous contexts.
   *
   * ## Engines
   * 
   * | Engine name | Implementation |
   * | :----- | :------------- |
   * | `node` | Uses `require('async_hooks').AsyncLocalStorage` |
   */
  asyncLogging: {enumerable: true, get: () => (engine) => {
    asyncLogging(engine);
    return makeLens;
  }},
  
  Factory: {enumerable: true, get: () => require('./cjs/lens_factory').default},
  JsContainerFactory: {enumerable: true, get: () => require('./cjs/js_container_factory').default},
  jsContainers: {enumerable: true, get: () => require('./cjs/js_container_factory').DEFAULT_FACTORY},
  polyfillImmutable: {enumerable: true, get: () => require('./cjs/immutable_support').polyfillImmutable},
  Step: {enumerable: true, get: () => require('./cjs/custom_step').default},
});

/**
 * @constant
 * @name module:natural-lenses#at_maybe
 * @type {symbol}
 *
 * @description
 * This constant is a key used for querying a method from container objects of
 * type `function(*): {@link Maybe}.<*>`.  The value passed to this method will be
 * a *key* from a Lens -- the kind of value that should be passed for a
 * conceptual "indexing" of the container.  For Object, this is a string property
 * name.  For Array, this is an integer index, where negative values count
 * backward from the end of the Array.  For Map, this uses `Map.prototype.has`
 * and `Map.prototype.get`.
 */

/**
 * @constant
 * @name module:natural-lenses#clone
 * @type {symbol}
 *
 * @description
 * This constant is a key used for querying a method from container objects of
 * type `function({set: {0: *, 1: *}?, spliceOut: *?}): *`.  The intent of the
 * method is to clone the container with some kind of alteration -- either
 * a key/index set to the given value in the clone, or a key/index deleted
 * from the clone.
 *
 * If the operation description passed contains a `set` property,
 * the value of that property should be an Array where element 0 is a key or
 * index into the container and element 1 is the value to set (cf. arguments
 * to `Map.prototype.set`).
 *
 * If the operation description passed contains a `spliceOut` property,
 * the value of that property should be a key or index to delete from the
 * container.  Where possible, the result should be to leave the container
 * in a state where `container[at_maybe](key)` returns `{}` (a *Nothing*).
 * This is specifically a problem for [immutable.List]{@link external:immutable.List},
 * which offers only a dense presentation of elements: every non-negative index
 * less than `size` is a valid and "contained" entry.  The implementation provided
 * by this library for implementing this method on
 * [immutable.List]{@link external:immutable.List} sets the value of the entry
 * in the clone to `undefined`.
 *
 * In the provided implemenation for Array, negative indexes are interpreted
 * counting backward from the end of the Array, as with `Array.prototype.slice`.
 *
 * `Symbol.species` is honored for determining the constructor used for the
 * clone; Object is a special case that defaults to Object if `Symbol.species`
 * is not present.
 */

/**
 * @constant
 * @name module:natural-lenses#isLens
 * @type {symbol}
 *
 * @description
 * This property is set on every kind of object to be recognized by this
 * library as implementing lens-like behavior.  Setting this property to
 * any truthy value on your own objects will cause this library to treat it
 * in many ways like a {@link Lens}.
 */

module.exports = makeLens;