chain-able
Version:
interfaces that describe their intentions.
672 lines (627 loc) • 16.3 kB
JavaScript
/* eslint no-new-wrappers: "off" */
/* eslint eqeqeq: "off" */
/* eslint func-style: "off" */
/* eslint complexity: "off" */
const isObjStrict = require('./is/objStrict')
const isRegExp = require('./is/regexp')
const isError = require('./is/error')
const isBoolean = require('./is/boolean')
const isNumber = require('./is/number')
const isString = require('./is/string')
const isDate = require('./is/date')
const isUndefined = require('./is/undefined')
const isArray = require('./is/array')
const isMap = require('./is/map')
const isSet = require('./is/set')
const argumentor = require('./argumentor')
const ObjectKeys = require('./util/keys')
const hasOwnProperty = require('./util/hasOwnProperty')
const getPrototypeOf = require('./util/getPrototypeOf')
const reduce = require('./reduce')
const toarr = require('./to-arr')
/**
* @param {Array | Object | any} xs
* @param {Function} fn
* @TODO: unexpectedly breaks things iterating
* if you are relying on internal functionality
* (such as .path, .get, .value...) with map & set
*
* @NOTE if there is .forEach on the obj already, use it
* otherwise, call function for each
*
*/
var forEach = function(xs, fn) {
if (xs.forEach) xs.forEach(fn)
else for (let i = 0; i < xs.length; i++) fn(xs[i], i, xs)
}
/**
* {@link https://sourcemaking.com/design_patterns/chain_of_responsibility chainofresponsibility}
*
* @param {Traversable} obj object to traverse
*
* @constructor
*
* @example
*
* traverse({})
* //=> new Traverse(obj)
*
*/
var traverse = function(obj) {
return new Traverse(obj)
}
module.exports = traverse
/**
* @func
* @class TraverseJS
* @classdesc Traverse and transform objects by visiting every node on a recursive walk.
* @prop {any} value
*
* @category traverse
* @memberOf Traverse
* @see deps/traverse
* @category traverse
* @types traverse
* @tests traverse/*
*
* @TODO: symbol, map, set
* @tutorial https://github.com/substack/js-traverse
*
* @param {Traversable} obj any traversable value
*
* @example
*
* traverse({})
* //=> Traverser
*
*/
function Traverse(obj) {
this.value = obj
}
/**
* @desc Get the element at the array path.
*
* @param {Array<string>} ps paths
* @return {any} value at dot-prop
*
* @memberOf Traverse
* @see this.forEach
* @todo hasOwnProperty
*/
Traverse.prototype.get = function(ps) {
let node = this.value
for (let i = 0; i < ps.length; i++) {
const key = ps[i]
if (!node || !hasOwnProperty(node, key)) {
node = undefined
break
}
node = node[key]
}
return node
}
/**
* @desc Return whether the element at the array path exists.
*
* @param {Array<string>} pathsArray paths
* @return {boolean} has element at path
*
* @memberOf Traverse
* @see hasOwnProperty
*
* @example
*
* traverse({eh: true}).has(['eh'])
* //=> true
*
* @example
*
* traverse({eh: true}).has(['canada'])
* //=> false
*
*
* @example
*
* traverse([0]).has([2])
* //=> false
*
*/
Traverse.prototype.has = function(pathsArray) {
let node = this.value
for (let i = 0; i < pathsArray.length; i++) {
const key = pathsArray[i]
if (!node || !hasOwnProperty(node, key)) {
return false
}
node = node[key]
}
return true
}
/**
* @desc Set the element at the array path to value.
*
* @param {Array<string>} arrayPath paths
* @param {any} value any value to assign to the element @ the path
* @return {any} value passed in
*
* @memberOf Traverse
* @see deps/dot
*/
Traverse.prototype.set = function(arrayPath, value) {
let node = this.value
let i = 0
for (; i < arrayPath.length - 1; i++) {
const key = arrayPath[i]
if (!hasOwnProperty(node, key)) node[key] = {}
node = node[key]
}
node[arrayPath[i]] = value
return value
}
/**
* @desc Execute fn for each node in the object and return a new object with the results of the walk. To update nodes in the result use this.update(value).
*
* @method
* @memberOf Traverse
* @see walk
* @param {Function} cb fn for each node in the object
* @return {any}
*
* @example
* var {traverse} = require('chain-able')
*
* var obj = {a: 1, b: 2, c: [3, 4]}
* obj.c.push(obj)
*
* var scrubbed = traverse(obj).map(function(x) {
* if (this.circular) this.remove()
* })
* console.dir(scrubbed)
* //=> { a: 1, b: 2, c: [ 3, 4 ] }
*/
Traverse.prototype.map = function(cb) {
return walk(this.value, cb, true)
}
/**
* @desc Execute fn for each node in the object but unlike .map(), when this.update() is called it updates the object in-place.
* executes a provided function once for each traversed element.
*
* @param {Function} callback provided callback function
* @return {any} this.value
*
* @memberOf Traverse
* @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/forEach
*
* @example
*
* var {traverse} = require('chain-able')
*
* var obj = [5, 6, -3, [7, 8, -2, 1], {f: 10, g: -13}]
* traverse(obj).forEach(function(x) {
* if (x < 0) this.update(x + 128)
* })
*
* console.dir(obj)
* //=> [ 5, 6, 125, [ 7, 8, 126, 1 ], { f: 10, g: 115 } ]
*
*/
Traverse.prototype.forEach = function(callback) {
this.value = walk(this.value, callback, false)
return this.value
}
/**
* @desc applies a function against an accumulator and each element in the array (from left to right) to reduce it to a single value.
* calls cb for each loop that is .notRoot
* defaults initial value to `this.value`
*
* @param {Function} cb callback forEach
* @param {Object | Array | any} init initial value
* @return {Object | Array | any}
*
* @see https://en.wikipedia.org/wiki/Fold_(higher-order_function)
* @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/Reduce
* @memberOf Traverse
*
* @example
*
* var {traverse} = require('chain-able')
*
* var obj = {
* a: [1, 2, 3],
* b: 4,
* c: [5, 6],
* d: {e: [7, 8], f: 9},
* }
*
* var leaves = traverse(obj).reduce(function(acc, x) {
* if (this.isLeaf) acc.push(x)
* return acc
* }, [])
*
* console.dir(leaves)
* //=> [ 1, 2, 3, 4, 5, 6, 7, 8, 9 ]
*
*/
Traverse.prototype.reduce = function(cb, init) {
const skip = arguments.length === 1
let acc = skip ? this.value : init
this.forEach(function(x) {
if (!this.isRoot || !skip) {
acc = cb.call(this, acc, x)
}
})
return acc
}
/**
* @desc Return an Array of every possible non-cyclic path in the object. Paths are Arrays of string keys.
* @return {Array<string>}
* @memberOf Traverse
* @tests traverse/keys
*/
Traverse.prototype.paths = function() {
const acc = []
this.forEach(function(x) {
acc.push(this.path)
})
return acc
}
/**
* @desc Return an Array of every node in the object.
* @memberOf Traverse
* @return {Array<any>}
*/
Traverse.prototype.nodes = function() {
const acc = []
this.forEach(function(x) {
acc.push(this.node)
})
return acc
}
/**
* @desc Create a deep clone of the object.
*
* @return {any}
*
* @memberOf Traverse
*
* @example
* const {traverse, eq} = require('chain-able')
*
* const obj = {eh: true, canada: [1]}
* const cloned = traverse(obj).clone()
* cloned.eh = false
* eq(cloned, obj)
* //=> false
*
*/
Traverse.prototype.clone = function() {
let parents = []
let nodes = []
return (function clone(src) {
for (let i = 0; i < parents.length; i++) {
if (parents[i] === src) {
return nodes[i]
}
}
if (isObjStrict(src)) {
let dst = copy(src)
parents.push(src)
nodes.push(dst)
forEach(ObjectKeys(src), key => {
dst[key] = clone(src[key])
})
parents.pop()
nodes.pop()
return dst
}
else {
return src
}
})(this.value)
}
/**
* @func
*
* @param {any} root root node
* @param {Function} cb callback for each
* @param {boolean} immutable should mutate or not
* @return {any}
*
* @see traverse.forEach
*/
function walk(root, cb, immutable) {
let path = []
let parents = []
let alive = true
/**
* @emits before
* @emits pre
* @emits post
* @emits after
*
* @param {any} node_
* @return {State} see types
*/
return (function walker(node_) {
// both are objs with properties that get changed but
const node = immutable ? copy(node_) : node_
const modifiers = {}
let keepGoing = true
/**
* Each method that takes a callback has a context (its this object) with these attributes:
* @prop {boolean} isRoot @alias isNotRoot Whether or not the present node is a leaf node (has no children)
* @type {Object}
*/
const state = {
/**
* The present node on the recursive walk
* @type {Array}
*/
node,
/**
* @see traverse.context.node
* @protected
* @type {Array}
*/
node_,
/**
* An array of string keys from the root to the present node
* @type {Array}
*/
path: [].concat(path),
/**
* The context of the node's parent. This is undefined for the root node.
* @type {undefined | Primitive}
*/
parent: parents[parents.length - 1],
parents,
/**
* The name of the key of the present node in its parent. This is undefined for the root node.
* @type {undefined | Primitive}
*/
key: path.slice(-1)[0],
/**
* Whether the present node is the root node
* @type {Boolean}
*/
isRoot: path.length === 0,
/**
* Depth of the node within the traversal
* @type {number}
*/
level: path.length,
/**
* If the node equals one of its parents, the circular attribute is set to the context of that parent and the traversal progresses no deeper.
* @type {null | boolean}
*/
circular: null,
/**
* Set a new value for the present node.
* All the elements in value will be recursively traversed unless stopHere is true.
*
* @param {Function} x
* @param {boolean} stopHere
* @return {void}
*/
update(x, stopHere) {
if (!state.isRoot) {
state.parent.node[state.key] = x
}
state.node = x
if (stopHere) keepGoing = false
},
/**
* Delete the current element from its parent in the output. Calls delete even on Arrays.
* @param {boolean} stopHere
* @return {void}
*/
delete(stopHere) {
delete state.parent.node[state.key]
if (stopHere) keepGoing = false
},
/**
* 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.
* @param {boolean} stopHere
* @return {void}
*/
remove(stopHere) {
// @NOTE safety
if (isUndefined(state.parent)) {
return
}
else if (isArray(state.parent.node)) {
state.parent.node.splice(state.key, 1)
}
else {
delete state.parent.node[state.key]
}
if (stopHere) keepGoing = false
},
keys: null,
/**
* Call this function before any of the children are traversed.
* You can assign into this.keys here to traverse in a custom order.
* @param {Function} fn
* @return {any}
*/
before(fn) {
modifiers.before = fn
},
/**
* Call this function after any of the children are traversed.
* @param {Function} fn
* @return {any}
*/
after(fn) {
modifiers.after = fn
},
/**
* Call this function before each of the children are traversed.
* @param {Function} fn
* @return {any}
*/
pre(fn) {
modifiers.pre = fn
},
/**
* Call this function after each of the children are traversed.
* @param {Function} fn
* @return {any}
*/
post(fn) {
modifiers.post = fn
},
/**
* @modifies alive
* @protected
* @return {void}
*/
stop() {
alive = false
},
/**
* @modifies keepGoing
* @protected
* @return {void}
*/
block() {
keepGoing = false
},
}
if (!alive) return state
/**
* @desc updates if needed:
* @modifies keys
* @modifies circular
* @modifies isLeaf
* @modifies notLeaf
* @modifies notRoot
* @return {void}
*/
function updateState() {
if (isObjStrict(state.node)) {
if (!state.keys || state.node_ !== state.node) {
state.keys = ObjectKeys(state.node)
}
// @NOTE was ==
state.isLeaf = state.keys.length === 0
for (let i = 0; i < parents.length; i++) {
if (parents[i].node_ === node_) {
state.circular = parents[i]
break
}
}
}
else {
state.isLeaf = true
state.keys = null
}
state.notLeaf = !state.isLeaf
state.notRoot = !state.isRoot
}
updateState()
// @NOTE added last `,state` arg to not have it have to use `this`,
// but broke some things so moved to another fn
//
// use return values to update if defined
let ret = cb.call(state, state.node)
if (!isUndefined(ret) && state.update) state.update(ret)
if (modifiers.before) modifiers.before.call(state, state.node)
if (!keepGoing) return state
// when it's some sort of itertable object, loop it further
if (isObjStrict(state.node) && !state.circular) {
parents.push(state)
updateState()
forEach(state.keys, (key, i) => {
path.push(key)
if (modifiers.pre) modifiers.pre.call(state, state.node[key], key)
const child = walker(state.node[key])
if (immutable && hasOwnProperty(state.node, key)) {
state.node[key] = child.node
}
// @NOTE was ==
child.isLast = i === state.keys.length - 1
child.isFirst = i === 0
if (modifiers.post) modifiers.post.call(state, child)
path.pop()
})
parents.pop()
}
if (modifiers.after) modifiers.after.call(state, state.node)
return state
})(root).node
}
/**
* @func
* @TODO does not respect ObjectDescriptors
* @NOTE wicked ternary
* @param {any} src
* @return {any}
*/
function copy(src) {
// require('fliplog').data(src).bold('copying').echo()
if (isObjStrict(src)) {
let dst
// require('fliplog').underline('is obj').echo()
if (isMap(src)) {
dst = reduce(src.entries())
}
else if (isSet(src)) {
dst = toarr(src)
}
if (isArray(src)) {
dst = []
}
else if (isDate(src)) {
dst = new Date(src.getTime ? src.getTime() : src)
}
else if (isRegExp(src)) {
dst = new RegExp(src)
}
else if (isError(src)) {
dst = {message: src.message}
}
else if (isBoolean(src)) {
dst = new Boolean(src)
}
else if (isNumber(src)) {
dst = new Number(src)
}
else if (isString(src)) {
dst = new String(src)
}
else {
//if (Object.create && Object.getPrototypeOf)
dst = Object.create(getPrototypeOf(src))
}
// else if (src.constructor === Object) {
// dst = {}
// }
// else {
// // @NOTE: only happens if above getPrototypeOf does not exist
// var proto = (src.constructor && src.constructor.prototype) ||
// src.__proto__ || {}
// var T = function() {}
// T.prototype = proto
// dst = new T()
// }
forEach(ObjectKeys(src), key => {
dst[key] = src[key]
})
return dst
}
else {
// require('fliplog').red('is NOT OBJ').echo()
return src
}
}
/**
* @desc adds methods to Traverser
*/
forEach(ObjectKeys(Traverse.prototype), key => {
traverse[key] = function(obj) {
const t = new Traverse(obj)
// args = argumentor.apply(null, arguments).slice(1)
return t[key].apply(t, argumentor.apply(null, arguments).slice(1))
}
})