Skip to content

Commit

Permalink
add preserveTypeIfTargetUnset option
Browse files Browse the repository at this point in the history
  • Loading branch information
patrickkunka committed Aug 27, 2018
1 parent 5288cd1 commit 0e1308d
Show file tree
Hide file tree
Showing 8 changed files with 118 additions and 11 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
# v0.2.0

- Adds new configuration option `preserveTypeIfTargetUnset` allowing preservation of custom types (e.g. consumer classes), when they can not be inferred from the target.

# v0.1.3

- Fixes issue where library erroneously detected client-side environment when running on Node.js.
Expand Down
57 changes: 57 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ The `merge()` function accepts an optional third parameter of configuration opti
errorMessage: Messages.MERGE_ERROR,
includeNonEnumerable: false,
includeReadOnly: false
preserveTypeIfTargetUnset: false,
useReferenceIfArray: false,
useReferenceIfTargetUnset: false,
}
Expand Down Expand Up @@ -117,6 +118,7 @@ merge(target, source, true);
- [errorMessage](#errormessage)
- [includeNonEnumerable](#includenonenumerable)
- [includeReadOnly](#includereadonly)
- [preserveTypeIfTargetUnset](#preserveTypeIfTargetUnset)
- [useReferenceIfArray](#usereferenceifarray)
- [useReferenceIfTargetUnset](#usereferenceiftargetunset)
Expand Down Expand Up @@ -337,6 +339,61 @@ console.log(source.fullName); // 'Jill Kay'
console.log(target.fullName); // 'Jill Kay'
```
### `preserveTypeIfTargetUnset`
| Type | Default |
|-----------|---------|
| `boolean` | `false` |
An optional boolean dictating whether or not to attempt preservation of custom types, when performing a deep merge into a `null` (unset) target value.
For example, property `source.foo` may be an instance of consumer class `Foo`, and property `target.foo` may be set to `null`. By default in this scenario, the individual properties and values of `source.foo` would be copied onto a new plain object (`{}`) which would be assigned to `target.foo`.
If set to `true`, the helpful merge will attempt to derive the custom type (`Foo`) of `source.foo`, assign a new instance of it to `target.foo`, and then copy all values across.
This is particularly useful when read-only computed properties ("getters") are present on the source object and should be maintained on the target object.
##### Example 1: Deep copying into a nested unset target (default behavior)
```js
class Foo {}

const source = {
foo: new Foo()
}

const target = {
foo: null
};

merge(target, source, true);

assert.isOk(target.foo); // true
assert.instanceOf(source.foo, Foo); // true
assert.instanceOf(target.foo, Foo); // false
```
##### Example 2: Deep copying into a nested unset target with type preservation
```js
class Foo {}

const source = {
foo: new Foo()
}

const target = {
foo: null
};

merge(target, source, {
deep: true,
preserveTypeIfTargetUnset: true
});

assert.isOk(target.foo); // true
assert.instanceOf(source.foo, Foo); // true
assert.instanceOf(target.foo, Foo); // true
```
### `useReferenceIfArray`
| Type | Default |
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "helpful-merge",
"version": "0.1.3",
"version": "0.2.0",
"description": "A highly-configurable merge implementation with intelligent error handling for validating consumer-provided input against configuration interfaces.",
"author": "KunkaLabs Limited",
"private": false,
Expand Down
1 change: 1 addition & 0 deletions src/Config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ class Config implements IConfig {
public deep: boolean = false;
public useReferenceIfTargetUnset: boolean = false;
public useReferenceIfArray: boolean = false;
public preserveTypeIfTargetUnset: boolean = false;
public includeReadOnly: boolean = false;
public includeNonEmurable: boolean = false;
public arrayStrategy: ArrayStrategy = ArrayStrategy.REPLACE;
Expand Down
1 change: 1 addition & 0 deletions src/Interfaces/IConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ interface IConfig {
deep?: boolean;
useReferenceIfTargetUnset?: boolean;
useReferenceIfArray?: boolean;
preserveTypeIfTargetUnset?: boolean;
includeReadOnly?: boolean;
includeNonEmurable?: boolean;
arrayStrategy?: ArrayStrategy;
Expand Down
9 changes: 9 additions & 0 deletions src/deriveCustomTypeInstance.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
function deriveCustoTypeInstance({constructor}: any): any {
if (typeof constructor === 'function' && constructor !== Object) {
return new constructor();
}

return {};
}

export default deriveCustoTypeInstance;
34 changes: 33 additions & 1 deletion src/merge.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ describe('merge()', () => {
assert.equal(obj1.foo.bar, 1);
});

it(`should add nested objects by references when deep merging,
it(`should add nested objects by references when deep merging, and
\`useReferenceIfTargetUnset\` option set, and target is unset`, () => {
const obj1 = {foo: null};
const obj2 = {foo: {bar: 1}};
Expand All @@ -98,6 +98,38 @@ describe('merge()', () => {
assert.equal(obj1.foo.bar, 1);
});

it(`should maintain the types of nested objects when deep merging, and
\`preserveTypeIfTargetUnset\` option set, and target is unset`, () => {
class Foo {}

const obj1 = {foo: null};
const obj2 = {foo: new Foo()};

merge(obj1, obj2, {
deep: true,
preserveTypeIfTargetUnset: true
});

assert.notEqual(obj1.foo, obj2.foo);
assert.instanceOf(obj1.foo, Foo);
assert.instanceOf(obj2.foo, Foo);
});

it(`should handle plain objects when deep merging, and
\`preserveTypeIfTargetUnset\` option set, and target is unset`, () => {
const obj1 = {foo: null};
const obj2 = {foo: {}};

merge(obj1, obj2, {
deep: true,
preserveTypeIfTargetUnset: true
});

assert.notEqual(obj1.foo, obj2.foo);
assert.instanceOf(obj1.foo, Object);
assert.instanceOf(obj2.foo, Object);
});

it('should skip read-only properties', () => {
const obj1: any = {};

Expand Down
21 changes: 12 additions & 9 deletions src/merge.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import Config from './Config';
import ArrayStrategy from './Constants/ArrayStrategy';
import FluentMerge from './FluentMerge';
import handleMergeError from './handleMergeError';
import IConfig from './Interfaces/IConfig';
import IMerge from './Interfaces/IMerge';
import * as Messages from './Messages';
import Config from './Config';
import ArrayStrategy from './Constants/ArrayStrategy';
import deriveCustomTypeInstance from './deriveCustomTypeInstance';
import FluentMerge from './FluentMerge';
import handleMergeError from './handleMergeError';
import IConfig from './Interfaces/IConfig';
import IMerge from './Interfaces/IMerge';
import * as Messages from './Messages';

function merge<T extends any>(target: T, source: any, options: (IConfig|true) = null): T {
const isClientSide = typeof window !== 'undefined';
Expand Down Expand Up @@ -91,10 +92,12 @@ function merge<T extends any>(target: T, source: any, options: (IConfig|true) =

if (!Object.prototype.hasOwnProperty.call(target, key) || target[key] === null) {
// If property does not exist on target, instantiate an empty
// object or array to merge into
// object, custom type or array to merge into

try {
target[key] = Array.isArray(source[key]) ? [] : {};
target[key] = Array.isArray(source[key]) ?
[] : config.preserveTypeIfTargetUnset ?
deriveCustomTypeInstance(source[key]) : {};
} catch (err) {
handleMergeError(err, target, key, config.errorMessage);
}
Expand Down

0 comments on commit 0e1308d

Please sign in to comment.