JSPM

  • Created
  • Published
  • Downloads 6857
  • Score
    100M100P100Q127240F
  • License MIT

Recursive, deep merge of anything (objects, arrays, strings or nested thereof), which weighs contents by type hierarchy to ensure the maximum content is retained

Package Exports

  • object-merge-advanced

This package does not declare an exports field, so the exports above have been automatically detected and optimized by JSPM instead. If any package subpath is missing, it is recommended to post an issue to the original package (object-merge-advanced) to support the "exports" field. If that is not possible, create a JSPM override to customize the exports field for this package.

Readme

object-merge-advanced

Standard JavaScript

Recursive, deep merge of anything (objects, arrays, strings or nested thereof), which weighs contents by type hierarchy to ensure the maximum content is retained

Build Status Coverage Status bitHound Overall Score bitHound Dependencies bitHound Dev Dependencies Downloads/Month

Table of Contents

Install

$ npm install --save object-merge-advanced

Purpose

It's like _.merge, but it correctly merges different-type things and behaves well when it encounters nested things like parsed HTML (lots of nested arrays, objects and strings).


Imagine, if we merged the identical keys of two objects judging their values by the hierarchy instead:

  • non-empty array trumps all below
  • non-empty plain object trumps all below
  • non-empty string ...
  • empty plain object ...
  • empty array
  • empty string
  • number
  • boolean
  • null
  • undefined doesn't trump anything

The idea is, we strive to retain as much info as possible after merging. For example, you'd be better off with a non-empty string than with an empty array or boolean.

The fun does not stop here. Sometimes life demands unidirectional merges from either source or destination ("overwrite no matter what, from either side"). This can be done per object-key-basis, see opts.ignoreKeys where first input object's key overrides the second's and opts.hardMergeKeys for the opposite.

That's what this library does

When object-merge-advanced merges two objects, it will check the types of their key values:

  • If a key exists only in one of the objects, it goes straight into the result object.
  • If a key exists on both, we got a clash. Key's value will be chosen judging by its value's type:
    • Arrays trump objects which trump strings which trump numbers which trump Booleans
    • Non-empty array as value trumps any object or string as value
    • Anything empty won't trump anything not empty
    • If both keys have plain object values, they'll get recursively fed back into the library again
    • Booleans will be merged using logical "OR"
    • Arrays will be merged, and if there are objects within, those objects will be merged smartly, depending if their keysets are similar. If not, objects will be merged as separate array elements.

There are ten possible combinations: 10 types of first input (object #1) and ten types of second input (object #2): non-empty (full) object, empty object, non-empty array, empty array, non-empty string, empty string, number, boolean, undefined and null.

matching algorithm

A Large number in the centre of a square shows which value prevails.

In the diagram above, the squares show which value gets assigned to the merge result — the first object's (marked 1, pink fields) or second one's (marked 2, sky blue fields).

In some cases, we perform a custom actions:

  1. passing value objects back into the main function recursively (when both values are plain objects),
  2. when merging arrays, we pay extra attention to the options object (if present) and the contents of both arrays (taking special measures for objects within),
  3. Logical "OR" composition (when both values are Boolean).

I challenge you to check test.js unit tests to see this library in action.

In practice

In practice I needed this library to normalise JSON files - generate a "schema" object (a superset of all used keys) and fill any missing keys within all JSON files. Also, JSON files get their keys sorted. That library is used to keep us sane when using JSON to store content for email templates - it's enough to add one unique key in one JSON, and all other templates' content files get it added as well.

I use unidirectional merging when dealing with content mapping JSON files which are by definition unidirectional-flow (always overwrite normal data JSON files).

Use

var mergeAdvanced = require('object-merge-advanced')

API

mergeAdvanced(input1, input2 [, { options }])

API - Input

Input argument Type Obligatory? Description
input1 Anything yes Normally an object literal, but array or string or whatever else will work too. Can be deeply nested.
input2 Anything yes Another thing, normally an object, but can be array or something else.
options Plain object no Optionally, pass all settings in a plain object, as a third argument
Options object's key Value Default Description
{
mergeObjectsOnlyWhenKeysetMatches Boolean true Controls the merging of the objects within arrays. See below.
ignoreKeys String n/a These keys, if present on input1, will be kept and not merged, that is, changed. You can use wildcards.
hardMergeKeys String n/a These keys, if present on input2, will overwrite their counterparts on input1 (if present) no matter what. You can use wildcards.
mergeArraysContainingStringsToBeEmpty Boolean false If any arrays contain strings, resulting merged array will be empty IF this setting is set to true.
oneToManyArrayObjectMerge Boolean false If one array has one object, but another array has many objects, when oneToManyArrayObjectMerge is true, each object from "many-objects" array will be merged with that one object from "one-object" array. Handy when setting defaults on JSON data structures.
}

mergeObjectsOnlyWhenKeysetMatches is an extra insurance from accidental merging two objects within arrays, where key sets are too different (both have at least one unique key).

For example:

Let's merge these two objects. Notice that each has a unique key (yyyy and xxxx in the object that sits within the first position of each array).

// #1
var obj1 = {
  a: [
    {
      a: 'a',
      b: 'b',
      yyyy: 'yyyy'
    }
  ]
}

var obj2 = {
  a: [
    {
      xxxx: 'xxxx',
      b: 'b',
      c: 'c'
    }
  ]
}

var res1 = mergeAdvanced(object1, object2)

console.log('res1 = ' + JSON.stringify(res1, null, 4))
// => {
//      a: [
//        {
//          a: 'a',
//          b: 'b',
//          yyyy: 'yyyy'
//        },
//        {
//          xxxx: 'xxxx',
//          b: 'b',
//          c: 'c'
//        }
//      ]
//    }

but if you turn off the safeguard, { mergeObjectsOnlyWhenKeysetMatches: false } each object within an array is merged no matter their differences in the keysets:

var res2 = mergeAdvanced(object1, object2, { mergeObjectsOnlyWhenKeysetMatches: false })
console.log('res2 = ' + JSON.stringify(res2, null, 4))
// => {
//      a: [
//        {
//          a: 'a',
//          b: 'b',
//          yyyy: 'yyyy',
//          xxxx: 'xxxx',
//          c: 'c'
//        }
//      ]
//    }

API - Output

A merged thing is returned. It's probably the same type of your inputs.

Objects or arrays in the inputs are not mutated. This is very important.

Testing

$ npm test

For unit tests we use AVA, Istanbul CLI and JS Standard notation.

I aim to have 100% code coverage (which is the case at the moment).

Difference from Lodash _.merge

Lodash _.merge gets stuck when encounters a mismatching type values within plain objects. It's not suitable for merging AST's, nor deep recursive merging.

Difference from object-assign

object-assign is just a hard overwrite of all existing keys, from one object to another. It does not weigh the types of the input values and will happily overwrite the string value with a boolean placeholder.

object-assign is not for merging data objects, it's for setting defaults in the options objects.

For example, in my email template builds, I import SCSS variables file as an object. I also import variables for each template, and template variables object overwrites anything existing in SCSS variables object.

That's because I want to be able to overwrite global colours per-template when needed.

Now imagine, we're merging those two objects, and SCSS variables object has a key "mainbgcolor": "#ffffff". Now, a vast majority of templates don't need any customisation for the main background, therefore in their content JSON files the key is set to default, Boolean false: "mainbgcolor": false.

If merging were done using object-assign, placeholder false would overwrite real string value "#ffffff. That means, HTML would receive "false" as a CSS value, which is pink!

If merging were done using object-merge-advanced, all would be fine, because String trumps Boolean — placeholder falses would not overwrite the default SCSS string values.

Contributing

All contributions are welcome. Please stick to Standard JavaScript notation and supplement the test.js with new unit tests covering your feature(s).

If you see anything incorrect whatsoever, do raise an issue. If you file a pull request, I'll do my best to help you to get it merged promptly. If you have any comments on the code, including ideas how to improve things, don't hesitate to contact me by email.

Licence

MIT License (MIT)

Copyright (c) 2017 Codsen Ltd, Roy Reveltas

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.