Why Gemfury? Push, build, and install  RubyGems npm packages Python packages Maven artifacts PHP packages Go Modules Debian packages RPM packages NuGet packages

Repository URL to install this package:

Details    
chain-able-chain / src / MergeChain.ts
Size: Mime:
/* eslint complexity: "OFF" */
const MethodChain = require('./MethodChain')
const ChainedMapBase = require('./ChainedMapBase')
const dopemerge = require('./deps/dopemerge')
const isFunction = require('./deps/is/function')
const isUndefined = require('./deps/is/undefined')
const isTrue = require('./deps/is/true')
const isMapish = require('./deps/is/mapish')
const ObjectKeys = require('./deps/util/keys')
const constructInit = require('./deps/fp/constructInit')
const EMPTY_OBJ = require('./deps/native/EMPTY_OBJ')
const SHORTHANDS_KEY = require('./deps/meta/SHORTHANDS_KEY')
const ENV_DEVELOPMENT = require('./deps/env/dev')
const ENV_DEBUG = require('./deps/env/debug')

const ON_EXISTING_KEY = 'onExisting'
const ON_VALUE_KEY = 'onValue'
const MERGER_KEY = 'merger'
const MERGER_OPTIONS_KEY = 'opts'
const OBJ_KEY = 'obj'

/**
 * @since 1.0.0
 * @type {Map}
 * @extends {ChainedMapBase}
 * @member MergeChain
 * @memberOf Chainable
 *
 * @types MergeChain
 * @tests MergeChain
 * @see deps/dopemerge
 *
 * {@link https://sourcemaking.com/design_patterns/visitor visitor-pattern}
 *
 * @TODO consider just making this a function,
 *       because 80/20 onValue merger & onExisting
 *       are rarely used & are easily overridable with .merge
 */
class MergeChain extends ChainedMapBase {
  /**
   * @static
   * @param  {Chainable | ParentType} parent ParentType required, for merging
   * @return {MergeChain} @chainable
   *
   * @example
   *
   *    let map = new Map()
   *    map.set('eh', 1)
   *    map.set('coo', 'oo')
   *
   *    MergeChain.init(map).merge({eh: 2})
   *    console.dir(map)
   *    //=> Map { 'eh' => 2, 'coo' => 'oo' }
   *
   */
  // static init(parent) {
  //   return new MergeChain(parent)
  // }

  /**
   * @inheritdoc
   */
  constructor(parent) {
    super(parent)

    /* prettier-ignore */
    this
      .extend([ON_EXISTING_KEY, ON_VALUE_KEY, OBJ_KEY])
      .set(ON_VALUE_KEY, () => true)
      .set(MERGER_KEY, dopemerge)
  }

  /**
   * @desc options for merging with dopemerge
   *       @modifies this.merger | this.opts
   *
   * @memberOf MergeChain
   * @since 1.0.2
   * @param  {Object | Function} opts when object: options for the merger. when function: is the merger
   * @return {MergeChain} @chainable
   * @see dopemerge
   *
   * @example
   *   {
   *     stringToArray: true,
   *     boolToArray: false,
   *     boolAsRight: true,
   *     ignoreTypes: ['null', 'undefined', 'NaN'],
   *     debug: false,
   *   }
   *
   * @example
   *    .merger(require('lodash.mergewith')())
   */
  merger(opts) {
    if (isFunction(opts)) return this.set(MERGER_KEY, opts)
    else return this.set(MERGER_OPTIONS_KEY, opts)
  }

  // [v] messes comments on conditional brace style
  /* prettier-ignore */
  /**
   * @desc merges object in, goes through all keys, checks cbs, dopemerges
   *
   * @since 1.0.0
   *
   * @param  {Object} [obj2=undefined] object to merge in, defaults to this.get('obj')
   * @return {MergeChain} @chainable
   *
   * @see ChainedMap
   * @TODO issue here if we extend without shorthands &
   *       we want to merge existing values... :s
   *
   *
   * @example
   *
   *  const chain = new Chain()
   *  chain.merge({canada: {eh: true}})
   *  chain.merge({canada: {arr: [0, {'1': 2}], eh: {again: true}}})
   *  chain.entries()
   *  //=> {canada:{ eh: {again: true}, arr: [0, {'1': 2}] }}
   *
   */
  merge(obj2) {
    // better uglifying
    const parent = this.parent
    const get = key => this.get(key)

    const onExisting = get(ON_EXISTING_KEY)
    const onValue = get(ON_VALUE_KEY)
    const opts = get(MERGER_OPTIONS_KEY)
    const obj = obj2 || get(OBJ_KEY)
    const merger = get(MERGER_KEY)
    const shorthands = parent.meta ? parent.meta(SHORTHANDS_KEY) : EMPTY_OBJ
    const keys = ObjectKeys(obj)

    // @@debugger

    /* istanbul ignore next: devs */
    if (ENV_DEVELOPMENT) {
      if (!obj) {
        console.log({onExisting, opts, obj, merger, shorthands, keys, parent})
        throw new Error('must provide an object to merge')
      }
    }

    /**
     * @private
     *
     * since this would be slower
     * if I want to not have a speedy default when using .onExisting
     * should @note to use .extend
     * when using chains without a class & doing .merge (edge-case)
     *
     * @param  {Primitive} key key (shorthands[key] or just key)
     * @param  {*} value obj[key]
     * @return {void}
     *
     * @TODO could use .eq here
     * @TODO if (isMapish(obj)) obj = obj.entries()
     *
     * @example
     *  var obj = {key: 1}
     *
     *  MergeChain.init(obj).merge({key: ['value']})
     *
     *  // goes to this internal scoped function
     *  handleExisting('key', ['value'])
     *  // if there is .onValue or .onExisting, use them, default deepmerge
     *
     *  obj
     *  //=> {key: [1, 'value']}
     *
     */
    const handleExisting = (key, value) => {
      /**
       * @desc when fn is a full method, not an extended shorthand
       * @since 0.5.0
       *
       * @param {Primitive} keyToSet key we chose to set
       * @param {*} valueToSet value we chose to set (merged, existing, new)
       * @return {Parent | Chain | *} .set or [keyToSet] return
       *
       * @example
       *
       *    MergeChain.init(new Chain().extend(['eh']))
       *
       *    //isFunction: true => call parent[keyToSet](valueToSet)
       *    setChosen('eh', 1)
       *    //=> parent
       *    parent.get('eh')
       *    //=> 1
       *
       *    //=>isFunction: false => parent.set(keyToSet, valueToSet)
       *    setChosen('oh', 1)
       *    //=> parent //<- unless .set is overriden
       *    parent.get('oh')
       *    //=> 1
       *
       */
      const setChosen = (keyToSet, valueToSet) =>
        (isFunction(parent[key])
          ? parent[keyToSet](valueToSet)
          : parent.set(keyToSet, valueToSet))

      /**
       * check if it's shorthanded
       * -> check if it has a value already
       */
      if (isTrue(parent.has(key))) {
        // get that value
        const existing = parent.get(key)

        /**
         * if we have onExisting, call it
         * else default to dopemerge
         */
        if (isUndefined(onExisting)) {
          /* istanbul ignore next: devs */
          if (ENV_DEBUG) {
            console.log(
              'parent has: no onExisting',
              {existing, [key]: value}
            )
          }
          setChosen(key, merger(existing, value, opts))
        }
        else {
          /* istanbul ignore next: devs */
          if (ENV_DEBUG) {
            console.log(
              'parent has: has onExisting',
              {existing, onExisting, [key]: value}
            )
          }

          /**
           * maybe we should not even have `.onExisting`
           * since we can just override merge method...
           * and then client can just use a custom merger...
           *
           * could add and remove subscriber but that's overhead and
           * tricky here, because if we set a value that was just set...
           */
          setChosen(key, onExisting(existing, value, opts))
        }
      }
      else {
        /* istanbul ignore next: devs */
        if (ENV_DEBUG) {
          console.log('parent does not have', {[key]: value})
        }
        setChosen(key, value)
      }
    }

    for (let k = 0, len = keys.length; k < len; k++) {
      // key to the current property in the data being merged
      let key = keys[k]

      // we have our value, no we can change the key if needed for shorthands
      const value = obj[key]

      // @NOTE: when shorthands is an object, key is the method it should call
      if (!isUndefined(shorthands[key]) && shorthands[key] !== key) {
        /* istanbul ignore next: devs */
        if (ENV_DEBUG) {
          console.log(
            'had a shorthand with a diff key than the object (likely @alias)',
            {shorthandMethod: shorthands[key], key, value}
          )
        }
        key = shorthands[key]
      }

      // method for the key
      const method = parent[key]

      /* istanbul ignore next: sourcemaps trigger istanbul here incorrectly */
      // use onValue when set
      if (!onValue(value, key, this)) {
        /* istanbul ignore next: devs */
        if (ENV_DEBUG) {
          console.log('had onValue, was false, ignored', {onValue, key, value})
        }
        continue
      }
      // when property itself is a Chainable
      else if (isMapish(method)) {
        /* istanbul ignore next: devs */
        if (ENV_DEBUG) {
          console.log('has method or shorthand')
        }
        parent[key].merge(value)
      }
      // we have a method or shorthand
      else if (method) {
        /* istanbul ignore next: devs */
        if (ENV_DEBUG) {
          console.log('has method or shorthand', {method, key, value})
        }
        handleExisting(key, value)
      }
      // default to .set on the store
      else {
        /* istanbul ignore next: devs */
        if (ENV_DEBUG) {
          console.log('went to default', {method, key, value})
        }
        parent.set(key, value)
      }
    }

    return parent
  }
}

constructInit(MergeChain)

/**
 * @memberOf MergeChain
 * @method onExisting
 * @since 0.9.0
 * @example
 *
 *    const {Chain, MergeChain} = require('chain-able')
 *
 *    const chain = new Chain().set('str', 'stringy')
 *
 *    MergeChain.init(chain)
 *      .onExisting((a, b) => a + b)
 *      .merge({str: '+'})
 *
 *    chain.get('str')
 *    //=> 'stringy+'
 *
 */

module.exports = MergeChain

// @TODO re-enable this later
// module.exports = new MethodChain(MergeChain.prototype)
//   .methods(['onExisting', 'onValue', 'obj'])
//   .build(MergeChain)