Skip to content

Commit

Permalink
refactor(lodash): merge (algolia/algoliasearch-helper-js#694)
Browse files Browse the repository at this point in the history
* test(merge): original lodash tests

* refactor(lodash): merge

disable some tests we don't want to comply to

* docs(merge): add jsdoc

* test: remove skipped tests

* test: rewrite title
  • Loading branch information
Haroenv committed Nov 18, 2019
1 parent 78a4c62 commit d2e2ee8
Show file tree
Hide file tree
Showing 5 changed files with 241 additions and 9 deletions.
3 changes: 1 addition & 2 deletions packages/algoliasearch-helper/src/SearchParameters/index.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
'use strict';

var merge = require('lodash/merge');

var merge = require('../functions/merge');
var defaultsPure = require('../functions/defaultsPure');
var find = require('../functions/find');
var valToNumber = require('../functions/valToNumber');
Expand Down
3 changes: 1 addition & 2 deletions packages/algoliasearch-helper/src/SearchResults/index.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
'use strict';

var merge = require('lodash/merge');

var merge = require('../functions/merge');
var defaultsPure = require('../functions/defaultsPure');
var orderBy = require('../functions/orderBy');
var compact = require('../functions/compact');
Expand Down
58 changes: 58 additions & 0 deletions packages/algoliasearch-helper/src/functions/merge.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
'use strict';

function clone(value) {
if (typeof value === 'object' && value !== null) {
return merge(Array.isArray(value) ? [] : {}, value);
}
return value;
}

/**
* This method is like Object.assign, but recursively merges own and inherited
* enumerable keyed properties of source objects into the destination object.
*
* NOTE: this behaves like lodash/merge, but:
* - does mutate functions if they are a source
* - treats non-plain objects as plain
* - does not work for circular objects
* - treats sparse arrays as sparse
* - does not convert Array-like objects to arrays
*
* @param {Object} object The destination object.
* @param {...Object} [sources] The source objects.
* @returns {Object} Returns `object`.
*/
function merge() {
var sources = Array.prototype.slice.call(arguments);
var target = sources.shift();

return sources.reduce(function(acc, source) {
if (acc === source) {
return acc;
}

if (source === undefined) {
return acc;
}

if (acc === undefined) {
return clone(source);
}

if (typeof acc !== 'object' && typeof acc !== 'function') {
return clone(source);
}

if (typeof source !== 'object' && typeof source !== 'function') {
return acc;
}

Object.keys(source).forEach(function(key) {
acc[key] = merge(acc[key], source[key]);
});

return acc;
}, target);
}

module.exports = merge;
13 changes: 8 additions & 5 deletions packages/algoliasearch-helper/src/requestBuilder.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use strict';

var merge = require('lodash/merge');
var merge = require('./functions/merge');

var requestBuilder = {
/**
Expand Down Expand Up @@ -72,7 +72,7 @@ var requestBuilder = {
additionalParams.numericFilters = numericFilters;
}

return merge(state.getQueryParams(), additionalParams);
return merge({}, state.getQueryParams(), additionalParams);
},

/**
Expand Down Expand Up @@ -117,7 +117,7 @@ var requestBuilder = {
additionalParams.facetFilters = facetFilters;
}

return merge(state.getQueryParams(), additionalParams);
return merge({}, state.getQueryParams(), additionalParams);
},

/**
Expand Down Expand Up @@ -310,8 +310,11 @@ var requestBuilder = {
if (typeof maxFacetHits === 'number') {
searchForFacetSearchParameters.maxFacetHits = maxFacetHits;
}
var queries = merge(requestBuilder._getHitsSearchParams(stateForSearchForFacetValues), searchForFacetSearchParameters);
return queries;
return merge(
{},
requestBuilder._getHitsSearchParams(stateForSearchForFacetValues),
searchForFacetSearchParameters
);
}
};

Expand Down
173 changes: 173 additions & 0 deletions packages/algoliasearch-helper/test/spec/functions/merge.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
'use strict';
const merge = require('../../../src/functions/merge');

it('should merge `source` into `object`', function() {
var names = {
characters: [{name: 'barney'}, {name: 'fred'}]
};

var ages = {
characters: [{age: 36}, {age: 40}]
};

var heights = {
characters: [{height: '5\'4"'}, {height: '5\'5"'}]
};

var expected = {
characters: [
{name: 'barney', age: 36, height: '5\'4"'},
{name: 'fred', age: 40, height: '5\'5"'}
]
};

expect(merge(names, ages, heights)).toEqual(expected);
});

it('should work with four arguments', function() {
var expected = {a: 4};
var actual = merge({a: 1}, {a: 2}, {a: 3}, expected);

expect(actual).toEqual(expected);
});

it('should merge onto function `object` values', function() {
function Foo() {}

var source = {a: 1};
var actual = merge(Foo, source);

expect(actual).toEqual(Foo);
expect(Foo.a).toEqual(1);
});

it('should merge first source object properties to function', function() {
var fn = function() {};
var object = {prop: {}};
var actual = merge({prop: fn}, object);

expect(actual.prop).toBeInstanceOf(Function);
});

// TODO: differs from lodash, but seems to make more sense to me
it('should merge first and second source object properties to function', function() {
var fn = function() {};
var object = {prop: {dogs: 'out'}};
var actual = merge({prop: fn}, {prop: fn}, object);

expect(actual.prop).toBe(fn);
expect(actual.prop.dogs).toBe('out');
});

it('should merge onto non-plain `object` values', function() {
function Foo() {}

var object = new Foo();
var actual = merge(object, {a: 1});

expect(actual).toBe(object);
expect(object.a).toBe(1);
});

it('should assign `null` values', function() {
var actual = merge({a: 1}, {a: null});
expect(actual.a).toBe(null);
});

it('should not augment source objects for inner objects', function() {
var source1 = {a: [{a: 1}]};
var source2 = {a: [{b: 2}]};
var actual = merge({}, source1, source2);

expect(source1.a).toEqual([{a: 1}]);
expect(source2.a).toEqual([{b: 2}]);
expect(actual.a).toEqual([{a: 1, b: 2}]);
});

it('should not augment source objects for inner arrays', function() {
var source1 = {a: [[1, 2, 3]]};
var source2 = {a: [[3, 4]]};
var actual = merge({}, source1, source2);

expect(source1.a).toEqual([[1, 2, 3]]);
expect(source2.a).toEqual([[3, 4]]);
expect(actual.a).toEqual([[3, 4, 3]]);
});

it('should merge plain objects onto non-plain objects', function() {
function Foo(object) {
Object.assign(this, object);
}

var object = {a: 1};
var actual = merge(new Foo(), object);

expect(actual instanceof Foo).toBe(true);
expect(actual).toEqual(new Foo(object));

actual = merge([new Foo()], [object]);
expect(actual[0] instanceof Foo).toBe(true);
expect(actual).toEqual([new Foo(object)]);
});

it('should not overwrite existing values with `undefined` values of object sources', function() {
var actual = merge({a: 1}, {a: undefined, b: undefined});
expect(actual).toEqual({a: 1, b: undefined});
});

it('should not overwrite existing values with `undefined` values of array sources', function() {
var array = [1];
array[2] = 3;

var actual = merge([4, 5, 6], array);
var expected = [1, 5, 3];

expect(actual).toEqual(expected);

array = [1, undefined, 3];

actual = merge([4, 5, 6], array);
expect(actual).toEqual(expected);
});

it('should skip merging when `object` and `source` are the same value', function() {
var object = {};
var pass = true;

Object.defineProperty(object, 'a', {
configurable: true,
enumerable: true,
get: function() {
pass = false;
},
set: function() {
pass = false;
}
});

merge(object, object);
expect(pass).toBe(true);
});

it('should not convert objects to arrays when merging arrays of `source`', function() {
var object = {a: {'1': 'y', 'b': 'z', 'length': 2}};
var actual = merge(object, {a: ['x']});

expect(actual).toEqual({a: {
'0': 'x',
'1': 'y',
'b': 'z',
'length': 2
}});

actual = merge({a: {}}, {a: []});
expect(actual).toEqual({a: {}});
});

it('should not convert strings to arrays when merging arrays of `source`', function() {
var object = {a: 'abcde'};
var actual = merge(object, {a: ['x', 'y', 'z']});

expect(actual).toEqual({a: ['x', 'y', 'z']});
});

0 comments on commit d2e2ee8

Please sign in to comment.