Repository URL to install this package:
|
Version:
1.3.2 ▾
|
/* 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)