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-traverse / src / traverse.ts
Size: Mime:
// conditionals
/* eslint complexity: "OFF" */

// inlined rollup
/* eslint import/max-dependencies: "OFF" */

// one file
/* eslint max-lines: "OFF" */

// debug conditionals
/* eslint max-depth: "OFF" */

import {
  isEmpty,
  isTrue,
  isIteratable,
  isUndefined,
  isArray,
  isMap,
  isSet,
  isObj,
  isPrimitive,
  isNull,
} from 'chain-able-deps/dist/is'
import {
  // keys as ObjectKeys,
  reduce,
  toArr as toarr,
  set as dotSet,
  // from dopemerge, may need to export
  emptyTarget,
  copy,
  eq,
  addPoolingTo,
} from 'chain-able-deps'

const ObjectKeys = Object.keys
// const ENV_DEBUG = require('./env/debug')
const ENV_DEBUG = true

/**
 * {@link https://github.com/wmira/object-traverse/blob/master/index.js object-traverse}
 * {@link https://www.npmjs.com/browse/keyword/traverse traverse-js}
 * {@link https://www.npmjs.com/package/tree-walk tree-walk}
 * {@link https://www.npmjs.com/package/1tree 1tree}
 * {@link https://www.npmjs.com/package/pathway pathway}
 * {@link https://www.npmjs.com/package/@mojule/tree tree}
 * {@link http://web.archive.org/web/20160930054101/http://substack.net/tree_traversal tree-traversal-article}
 * {@link https://medium.com/@alexanderv/tries-javascript-simple-implementation-e2a4e54e4330 js-trie-medium}
 * --------------------
 *
 * if needed, clone
 *
 * first things to check are number/string/boolean/null/undefined
 *
 * then check non-iteratables
 * symbol, promise,
 *
 * then check conversions
 * - map, set
 *
 * then check empties
 * - obj
 * - fn
 *
 * -------
 *
 * numbers f-or first/last
 * and as a sort of hash like
 * 1 + 2 + 4 = ISLEAF & ISROOT ?
 *
 * Array
 *
 * Object Function Date Error Map Set
 *
 * String
 * Number NaN Infinity
 * Boolean
 *
 *
 * null undefined
 *
 * Promise Symbol
 *
 * ----
 *
 * @emits before
 * @emits pre
 * @emits post
 * @emits after
 */

/**
 * @desc Traverse class, pooled
 * @modifies this.node
 * @modifies this.parent
 * @modifies this.root
 * @since 5.0.0
 *
 * @member Traverse
 * @class
 * @constructor
 * @alias IterAteOr
 * @extends pooler
 *
 * @param {Traversable} iteratee value to iterate, clone, copy, check for eq
 * @param {Object | undefined} [config] wip config for things such as events or configs
 *
 * @see {@link tree-traversal-article}
 * @see traverse
 * @TODO make this a trie OR a linked-list
 *
 * @tests traverse
 * @types traverse
 *
 * @example
 *
 *    new Traverse([1])
 *    new Traverse([], {})
 *
 */
function Traverse(this: typeof Traverse, iteratee: any, config?: any) {
  // always cleared when done anyway
  if (isUndefined(this.parents)) this.parents = new Set()

  this.node = iteratee
  this.parent = iteratee
  this.root = iteratee
  this.reset()

  // to pass in the events (as commented below) without messing up scope?
  // if (config.on) this.on = config.on
  return this
}

/**
 * @desc reset the properties when finished pooling or instantiating
 * @since 5.0.0
 * @method
 *
 * @memberOf Traverse
 * @modifies Traverse.path
 * @modifies Traverse.key
 * @modifies Traverse.isAlive
 * @modifies Traverse.isCircular
 * @modifies Traverse.isLeaf
 * @modifies Traverse.isRoot
 * @modifies Traverse.depth
 * @return {void}
 *
 * @example
 *    traverse([]).reset()
 */
Traverse.prototype.reset = function() {
  this.path = []
  this.key = undefined
  this.isAlive = true
  this.isCircular = false
  this.isLeaf = false
  this.isRoot = true

  // iterates +1 so start at 0
  this.depth = -1
}

/**
 * @desc find parent,
 *       is there a parent
 *       above the current depth
 *       with the same value,
 *       making it circular?
 *
 * @memberOf Traverse
 * @since 5.0.0
 * @private
 * @method
 *
 * @param  {number} depth current depth, to find parent >=
 * @param  {parent} value parent value to find
 * @return {boolean} hasParent
 *
 * @example
 *
 *    var obj = {eh: ['eh']}
 *    traverse(obj).addParent(0, obj)
 *
 */
Traverse.prototype.hasParent = function(depth, value) {
  // or array
  return isObj(value) ? this.parents.has(value) : false
}

/**
 * @desc add parent, to prevent circular iterations
 * @memberOf Traverse
 * @since 5.0.0
 * @private
 * @method
 *
 * @param  {number} depth current depth, to add parent to >=
 * @param  {parent} value parent value to add
 * @return {void}
 *
 * @example
 *
 *    var obj = {eh: ['eh']}
 *    traverse(obj).addParent(0, obj)
 *
 */
Traverse.prototype.addParent = function(depth, value) {
  // && this.hasParent(value) === false
  if (isObj(value)) this.parents.add(value)
}

/**
 * @desc remove all parents, reset the map
 *
 * @memberOf Traverse
 * @since 5.0.0
 * @private
 * @method
 *
 * @return {void}
 *
 * @example
 *
 *    var obj = {eh: ['eh']}
 *    traverse(obj).forEach((key, value, t) => {
 *       t.parents
 *       //=> Set([obj])
 *       t.clear()
 *       t.parents
 *       //=> Set[]
 *    })
 *
 */
Traverse.prototype.clear = function() {
  // if (!isUndefined(this.parents))
  this.parents.clear()
}

/**
 * @memberOf Traverse
 * @since 5.0.0
 * @private
 * @method
 *
 * @param  {number} depth current depth, to find parents >=
 * @param  {parent} value parent value to remove
 * @return {void}
 *
 * @example
 *
 *    var obj = {eh: ['eh']}
 *    traverse(obj).removeParent(0, obj)
 *
 */
Traverse.prototype.removeParent = function(depth, value) {
  this.parents.delete(value)
}

/**
 * @desc this is the main usage of Traverse
 * @memberOf Traverse
 * @since 3.0.0
 * @version 5.0.0
 * @method
 *
 * @param  {Function} cb callback for each iteration
 * @return {*} mapped result or original value, depends how it is used
 *
 * @example
 *
 *    traverse([1, 2, 3]).forEach((key, value) => console.log({[key]: value}))
 *    //=> {'0': 1}
 *    //=> {'1': 2}
 *    //=> {'2': 3}
 *
 */
Traverse.prototype.forEach = function iterateForEach(cb) {
  /* istanbul ignore next: dev */
  if (ENV_DEBUG) {
    console.log('\n forEach \n')
  }

  const result = this.iterate(cb)

  // TODO: HERE, WHEN THIS IS ADDED, CAN BREAK SOME TESTS? SCOPED PARENTS MAP?
  Traverse.release(this)

  return result
}

/**
 * @desc stop the iteration
 * @modifies this.isAlive = false
 * @memberOf Traverse
 * @method
 *
 * @return {void}
 *
 * @example
 *
 *   traverse({eh: true, arr: []}).forEach((key, val, t) => {
 *      if (isArray(val)) this.stop()
 *   })
 *
 */
Traverse.prototype.stop = function stop() {
  this.isAlive = false
  // this.release()
}

/**
 * @TODO skip 1 branch
 * @version 5.0.0
 * @since 3.0.0
 * @memberOf Traverse
 * @method
 *
 * @return {void}
 *
 * @example
 *
 *    traverse([1, 2, 3, [4]]).forEach((key, val, t) => {
 *      if (isArray(val)) t.skip()
 *    })
 *
 */
Traverse.prototype.skip = function skip() {
  this.skipBranch = true
}

/* prettier-ignore */
/**
 * @desc checks whether a node is iteratable
 *       @modifies Traverse.isIteratable
 *       @modifies Traverse.isLeaf
 *       @modifies Traverse.isCircular
 *
 * @memberOf Traverse
 * @protected
 * @method
 *
 * @param  {*} node value to check
 * @return {void}
 *
 * @TODO move into the wrapper? if perf allows?
 *
 * @example
 *
 *    .checkIteratable({eh: true})
 *    //=> this.isLeaf = false
 *    //=> this.isCircular = false
 *    //=> this.isIteratable = true
 *
 *    .checkIteratable({} || [])
 *    //=> this.isLeaf = true
 *    //=> this.isCircular = false
 *    //=> this.isIteratable = false
 *
 *    var circular = {}
 *    circular.circular = circular
 *    .checkIteratable(circular)
 *    //=> this.isLeaf = false
 *    //=> this.isCircular = true
 *    //=> this.isIteratable = true
 *
 */
Traverse.prototype.checkIteratable = function check(node) {
  this.isIteratable = isIteratable(node)
  // just put these as an array?
  if (isTrue(this.isIteratable)) {
    // native = leaf if not root
    this.isLeaf = false
    const path = this.path.join('.')

    if (this.hasParent(path, node)) {
      /* istanbul ignore next: dev */
      if (ENV_DEBUG) {
        console.log('circular___________', {node, path: this.path})
      }
      this.isCircular = true
    }
    else {
      this.addParent(path, node)
      this.isCircular = false
    }

    /* istanbul ignore next: dev */
    if (ENV_DEBUG) {
      // console.log('IS_CIRCULAR_JSON', isCircular(node), this.isCircular, node)
    }
  }
  else {
    this.isLeaf = true
    this.isCircular = false
  }
}

/* prettier-ignore */
/**
 * Remove the current element from the output.
 * If the node is in an Array it will be spliced off.
 * Otherwise it will be deleted from its parent.
 *
 * @memberOf Traverse
 * @version 5.0.0
 * @since 2.0.0
 * @method
 *
 * @param {undefined | Object} [arg] optional obj to use, defaults to this.node
 * @return {void}
 *
 * @example
 *
 *    traverse([0]).forEach((key, val, it) => it.remove())
 *    //=> []
 *
 *    traverse({eh: true}).forEach((key, val, it) => it.remove())
 *    //=> {}
 *
 *    traverse({eh: true, str: 'stringy'}).forEach((key, val, it) => {
 *      if (!isString(val)) it.remove()
 *    })
 *    //=> {str: 'stringy'}
 *
 */
Traverse.prototype.remove = function removes(arg) {
  // ignore undefined & non-object/arrays
  if (isUndefined(this.key)) return
  let obj = arg || this.node
  if (!isObj(obj)) return

  /* istanbul ignore next: dev */
  if (ENV_DEBUG) {
    console.log('remove')
    console.log({parent: this.parent, key: this.key, obj})
  }

  this.removeParent(obj)
  this.skip()

  delete obj[this.key]
  delete this.parent[this.key]

  /* istanbul ignore next: dev */
  if (ENV_DEBUG) {
    console.log('traverse:remove:', this.key, {obj, iteratee: this.node})
  }
}

/**
 * @desc update the value for the current key
 * @version 5.0.0
 * @since 2.0.0
 * @memberOf Traverse
 *
 * @param  {*} value this.node[this.key] = value
 * @return {void}
 *
 * @example
 *
 *    traverse({eh: true})
 *    .forEach((key, val, traverser) => {
 *       if (this.isRoot) return
 *       traverser.update(false)
 *    })
 *    //=> {eh: false}
 *
 */
Traverse.prototype.update = function update(value) {
  dotSet(this.root, this.path, value)
}

/**
 * @desc mark the iteration as done, clear the map
 * @NOTE this recycles the instance in the pooler to re-use allocated objects
 * @memberOf Traverse
 * @private
 * @since 5.0.0
 *
 * @return {void}
 *
 * @see Traverse.iterate
 *
 * @example
 *
 *  traverse([]).destructor()
 *
 */
Traverse.prototype.destructor = function destructor() {
  this.node = undefined
  this.parent = undefined
  this.reset()

  this.clear()
}

/* prettier-ignore */
/**
 * @TODO handler for Set & Map so they can be skipped or traversed, for example when cloning...
 * @TODO add hook to add custom checking if isIteratable
 * @TODO deal with .isRoot if needed
 * @TODO examples with clone and stop
 *
 * @memberOf Traverse
 * @protected
 * @sig on(key: null | Primitive, val: any, instance: Traverse): any
 *
 * @param  {Function} on callback fn for each iteration
 * @return {*} this.node
 *
 * @example
 *
 *    iterate([])
 *    //=> []
 *    //=> on(null, [])
 *
 * @example
 *
 *    iterate([1])
 *    //=> [1]
 *    //=> on(null, [1])
 *    //=> on('1', 1)
 *
 * @example
 *
 *    //primitive - same for any number, string, symbol, null, undefined
 *    iterate(Symbol('eh'))
 *    //=> Symbol('eh')
 *    //=> on(Symbol('eh'))
 *
 * @example
 *
 *    var deeper = {eh: 'canada', arr: [{moose: true}, 0]}
 *    iterate(deeper)
 *    //=> deeper // returns
 *    //=> on(null, deeper, this) // root
 *
 *    //=> on('eh', 'canada', this) // 1st branch
 *
 *    //=> on('arr', [{moose: true}, 0], this)
 *    //=> on('arr.0', [{moose: true}], this)
 *    //=> on('arr.0.moose', true, this)
 *    //=> on('arr.1', [0], this)
 *
 *
 */
Traverse.prototype.iterate = function iterate(on) {
  /* istanbul ignore next : dev */
  if (ENV_DEBUG) {
    // require('fliplog')
    // .bold(this.path.join('.'))
    // .data(parents.keys())
    // .echo()
    console.log('\n...iterate...\n')
  }

  if (this.isAlive === false) {
    /* istanbul ignore next : dev */
    if (ENV_DEBUG) {
      console.log('DEAD')
    }

    return Traverse.release(this)
  }

  let node = this.node

  // convert to iteratable
  if (isMap(node)) {
    node = reduce(node)
  }
  else if (isSet(node)) {
    node = toarr(node)
  }

  // @TODO: maybe only right before sub-loop
  this.addParent(this.depth, node)

  const nodeIsArray = isArray(node)
  const nodeIsObj = nodeIsArray || isObj(node)

  // ---

  // @event
  if (!isUndefined(this.onBefore)) {
    // eslint-disable-next-line no-useless-call
    this.onBefore(this)
  }

  /* istanbul ignore next : dev */
  if (ENV_DEBUG) {
    // const str = require('pretty-format')({nodeIsObj, nodeIsArray, node})
    // require('fliplog').verbose(1).data({nodeIsObj, nodeIsArray, node}).echo()
    // console.log(node, parents)
    // console.log(str)
    console.log({nodeIsObj, nodeIsArray, node})
  }

  /**
   * call as root, helpful when we
   * - iterate something with no keys
   * - iterate a non-iteratable (symbol, error, native, promise, etc)
   */
  if (isTrue(this.isRoot)) {
    on.call(this, null, node, this)
    this.isRoot = false
  }

  const isObjOrArray = nodeIsArray || nodeIsObj

  // if (isObjOrArray) {
  //   // @event
  //   if (!isUndefined(this.onBefore)) {
  //     // eslint-disable-next-line no-useless-call
  //     this.onBefore(this)
  //   }
  // }

  // --------------------
  // IF OBJWITHOUTKEYS, IF ARRAY WITHOUT LENGTH...
  if (isObjOrArray && isEmpty(node)) {
    on.call(this, this.key, node, this)
    this.node = node
  }

  // --------------------

  else if (isObjOrArray) {
    this.depth = this.path.length

    // @TODO SAFETY WITH `props(node)` <- fixes Error
    let keys = nodeIsArray ? node : ObjectKeys(node)

    /* istanbul ignore next : dev */
    if (ENV_DEBUG) {
      console.log({keys})
      // require('fliplog').verbose(1).data(this).echo()
    }

    // @event
    // if (!isUndefined(this.onBefore)) this.onBefore()

    // @NOTE: safety here
    // this.checkIteratable(node)

    // const last = keys[keys.length - 1]

    // @loop
    for (let key = 0; key < keys.length; key++) {
      // if (ENV_DEBUG)
      // console.log('iterating:', {key})

      // --- safety ---
      if (this.isAlive === false) {
        /* istanbul ignore next : dev */
        if (ENV_DEBUG) {
          console.log('DEAD')
        }

        return Traverse.release(this)
      }

      // @NOTE: look above add prev add parent
      // addParent(this.depth, node)


      // ----- setup our data ----

      // to make it deletable
      if (node !== this.node) this.parent = node

      this.key = nodeIsArray ? key : keys[key]
      // this.isLast = key === last

      /* istanbul ignore next: dev */
      if (ENV_DEBUG) {
        console.log('alive', this.key)
      }

      // @event
      if (!isUndefined(this.onPre)) {
        // eslint-disable-next-line no-useless-call
        this.onPre(this)
      }


      const value = node[this.key]

      this.checkIteratable(value)
      // addParent(value)
      const pathBeforeNesting = this.path.slice(0)

      // @NOTE: can go forward-backwards if this is after the nested iterating
      this.path.push(this.key)
      this.depth = this.path.length

      // ----- continue events, loop deeper when needed ----

      // @NOTE since we check isAlive at the beginning of each loop
      // could use .skip alongisde .stop
      // @TODO @IMPORTANT @HACK @FIXME right here it should also handle the .stop
      on.call(this, this.key, value, this)
      if (isTrue(this.skipBranch)) {
        this.skipBranch = false
        continue
      }

      /* istanbul ignore next: dev */
      if (ENV_DEBUG) {
        // require('fliplog').data(parents).echo()
        // require('fliplog').data(this).echo()
      }

      // handle data
      if (isTrue(this.isCircular)) {
        /* istanbul ignore next: dev */
        if (ENV_DEBUG) {
          console.log('(((circular)))', this.key)
        }

        // on.call(this, this.key, value, this)
        // this.path.pop()
        this.path = pathBeforeNesting

        // this.isCircular = false
        // break
        continue
        // return
      }


      // &&
      if (isTrue(this.isIteratable)) {
        /* istanbul ignore next: dev */
        if (ENV_DEBUG) {
          console.log('(((iteratable)))', this.key)
        }

        this.node = value
        this.iterate(on)
        this.path = pathBeforeNesting
      }

      /* istanbul ignore next: dev */
      if (ENV_DEBUG) {
        if (this.isIteratable === false) {
          console.log('not iteratable', this.key)
        }

        console.log('----------------- post ----------', node)
      }

      // @event
      if (!isUndefined(this.onPost)) {
        // eslint-disable-next-line no-useless-call
        this.onPost(this)
      }

      // cleanup, backup 1 level
      this.path.pop()

      this.removeParent(node)
    }

    // this.path.pop()
    this.depth = this.path.length
  }
  else {
    // this.isLast = false
    on.call(this, this.depth, node, this)
  }

  // @NOTE: careful
  // removeParent(node)

  // @NOTE: just for .after ?
  this.node = node

  // @event
  if (!isUndefined(this.onAfter)) {
    // eslint-disable-next-line no-useless-call
    this.onAfter(this)
  }

  this.path.pop()

  return this.node
}

// is smaller, but probably slower
// function onEvent(property) {
//   return function(fn) {
//     this[property] = function
//   }
// }

// when it's some sort of itertable object, loop it further
// @TODO: need to handle these better without totally messing with bad scope
Traverse.prototype.pre = function(fn) {
  this.onPre = fn
}
Traverse.prototype.post = function(fn) {
  this.onPost = fn
}
Traverse.prototype.before = function(fn) {
  this.onBefore = fn
}
Traverse.prototype.after = function(fn) {
  this.onAfter = fn
}

// -----------------------

/**
 * @TODO merge with dopemerge?
 * @TODO needs tests converted back for this (observe tests do cover somewhat)
 *
 * @param  {*} arg defaults to this.node
 * @return {*} cloned
 *
 * @example
 *
 *   var obj = {}
 *   var cloned = traverse().clone(obj)
 *   obj.eh = true
 *   eq(obj, cloned)
 *   //=> false
 *
 */
Traverse.prototype.clone = clone

/**
 * @todo ugh, how to clone better with *recursive* objects?
 * @param  {any} src wip
 * @return {any} wip
 */
Traverse.prototype.copy = copy

/**
 * @desc clone any value
 * @version 5.0.0
 * @since 4.0.0
 * @memberOf Traverse
 * @extends copy
 * @extends Traverse
 *
 * @param  {*} arg argument to clone
 * @return {*} cloned value
 *
 * {@link http://underscorejs.org/#clone underscore-clone}
 * @see {@link underscore-clone}
 * @see dopemerge
 *
 * @example
 *
 *    var obj = {eh: true}
 *    clone(obj) === obj //=> false
 *
 *    var obj = {eh: true}
 *    var obj2 = clone(obj)
 *    obj.eh = false
 *    console.log(obj2.eh) //=> true
 *
 */
function clone(arg) {
  const obj = isUndefined(arg) ? this.node : arg
  if (isPrimitive(obj)) return obj
  let cloned = emptyTarget(obj)
  let current = cloned

  traverse(obj).forEach((key, value, traverser) => {
    // t.isRoot
    if (isNull(key)) return

    let copied = copy(value)
    if (traverser.isCircular && isArray(value)) copied = value.slice(0)
    dotSet(current, traverser.path, copied)
  })

  return cloned
}

// @TODO could just have traverse = Traverse.getPooled ?
const T = addPoolingTo(Traverse, undefined)
// Traverse.release = () => console.warn('removed pooling')
function traverse(value: any) {
  // @todo @@perf
  // return new Traverse(value)
  return T.getPooled(value)
}

// @todo thought we had `_eq = traverse(eq)`
traverse.eq = eq
traverse.clone = clone
traverse.copy = copy

export { eq, clone, copy, traverse, Traverse }
export default traverse