/**
* Dreamscapes\semantic-merge
*
* Licensed under the BSD-3-Clause license
* For full copyright and license information, please see the LICENSE file
*
* @author Robert Rossmann <rr.rossmann@me.com>
* @copyright 2015 Robert Rossmann
* @link https://github.com/Dreamscapes/semantic-merge
* @license http://choosealicense.com/licenses/bsd-3-clause BSD-3-Clause License
*/
'use strict'
/**
* The semantic-merge module. It merges stuff. Semantically.
*
* **Usage:**
*
* ```js
* var merge = require('semantic-merge')
* , source = { prop: 'I am in the source!' }
* , target = { prop: 'I am in the target, and will be overwritten :(' }
* , result
*
* result = merge(source).into(target)
* // Note that result is the same object as target. To perform a clone, do:
* result = merge(source).and(target).into({})
* ```
*
* @module semantic-merge
* @type {Function}
* @tutorial basics
*/
module.exports = Merger
/**
* @classdesc This is the Merger. It is implemented as a class, but you do not need to use it as
* one - simply call the constructor function as you would call any other function and
* you get the results you would expect.
*
* @summary Create a new merger
*
* @desc If called without `new`, it will create a new instance anyway.
*
* @class
* @param {Object} source The object to be merged
*/
function Merger (source) {
// Allow calling me without the `new` keyword
if (! this)
return new Merger(source)
// Define some non-enumerable properties to help keep track of merging
Object.defineProperties(
this
, { recurse:
{ value: false
, writable: true
}
, sources:
{ value: []
}
, exclusions:
{ value: []
}
}
)
// Push the source into the sources array
this.and(source)
}
/**
* @summary Add another source object to be merged
*
* @param {Object} source The object to be merged
* @return {this}
*/
Merger.prototype.and = function and (source) {
var type = typeOf(source)
// Only allow objects as sources
if (type !== 'object')
throw new TypeError('Object or array source is required for merging, ' + type + ' given')
this.sources.push(source)
return this
}
/**
* @summary Set the target object to merge into and perform the actual merge
*
* @desc This should be the last method called.
*
* @param {Object} target The object to start merging into
* @return {Object} Returns the merged object
*/
Merger.prototype.into = function into (target) {
var source
, property
, item
, type = typeOf(target)
// Only allow objects as targets
if (type !== 'object')
throw new TypeError('Object or array target is required for merging, ' + type + ' given')
// There may be multiple source objects to be merged into the target - start with the object which
// has been added to the sources as last
while (this.sources.length) {
source = this.sources.pop()
for (property in source) {
// Do not copy properties defined up in the prototype chain
if (! source.hasOwnProperty(property))
continue
// Is this property on the list of exclusions?
if (~ this.exclusions.indexOf(property))
continue
item = source[property] // Current item being merged
// Is this an array? Arrays should be merged by value, not by key
if (target instanceof Array) {
// If the item is not yet in the target array, add it
if (! ~ target.indexOf(item))
target.push(item)
continue
}
// Is this an object? Should we perform recursive merge?
if (this.recurse && typeOf(item) === 'object') {
// "To understand recursion, you must first understand recursion."
target[property] = new Merger(item)
.recursively
.excluding(this.exclusions)
// Only create new target object (of the same type) if the target does not exist yet
.into(typeOf(target[property]) === 'object' ? target[property] : new item.constructor())
continue
}
target[property] = item
}
}
return target
}
/**
* @summary While merging, ignore properties with this name
*
* @tutorial ignoring-properties
* @param {String|Array} properties A single property or an array of properties to be ignored.
* This will be used on all recursive levels of merging.
* @return {this}
*/
Merger.prototype.excluding = function excluding (properties) {
if (! (properties instanceof Array))
properties = [properties]
for (var i in properties)
this.exclusions.push(properties[i])
return this
}
/**
* @summary Alias of {@link module:semantic-merge~Merger#excluding excluding()}
*
* @tutorial ignoring-properties
* @method module:semantic-merge~Merger#except
* @return {this}
*/
Merger.prototype.except = Merger.prototype.excluding
/**
* @summary Semantic getter to enable recursive merge
*
* @desc By default, only shallow merge is performed. That means, if an object's property is
* also an object, it will be copied by reference. To avoid this, call this getter
* somewhere in your call chain (before you call
* {@link module:semantic-merge~Merger#into into()})
* @tutorial recursive-merge
* @member module:semantic-merge~Merger#recursively
* @default this
*/
Object.defineProperty(Merger.prototype, 'recursively'
, { enumerable: true
, get: function recursively () {
this.recurse = true
return this
}
}
)
/**
* @summary null-safe typeof
*
* @private
* @param {mixed} obj The item to get type of
* @return {String} The type of the item. Returns 'null' if item is null
*/
function typeOf (obj) {
return obj === null ? 'null' : typeof obj
}