Skip to content

Commit

Permalink
feat: add deepAssign array behavior options (#77)
Browse files Browse the repository at this point in the history
  • Loading branch information
RasPhilCo authored May 5, 2021
1 parent ee18019 commit 24456d2
Show file tree
Hide file tree
Showing 4 changed files with 279 additions and 6 deletions.
2 changes: 1 addition & 1 deletion src/asserters/json.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export class JsonHasPropertiesAsserter extends AsserterBase {
}
for (const targetJSONPath of jsonPaths) {
const targetJSON = require(targetJSONPath)
const assertedJSON = deepAssign({...targetJSON}, sourceJSON)
const assertedJSON = deepAssign({...targetJSON}, sourceJSON, {arrayBehavior: this.assertion.array_behavior})
await fs.writeFile(targetJSONPath, JSON.stringify(assertedJSON, null, 2) + '\n')
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/asserters/yaml.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export class YamlHasPropertiesAsserter extends AsserterBase {
const sourceYamlAsJson = yaml.load(await fs.readFile(path.join(this.templateDir, this.assertion.source_relative_filepath), 'utf-8')) as object
const targetYamlAsJson = yaml.load(await fs.readFile(targetYamlPath, 'utf-8')) as object

const assertedJson = deepAssign({...targetYamlAsJson}, sourceYamlAsJson)
const assertedJson = deepAssign({...targetYamlAsJson}, sourceYamlAsJson, {arrayBehavior: this.assertion.array_behavior})
const assertedYaml = yaml.dump(assertedJson, {indent: 2})
await fs.writeFile(targetYamlPath, assertedYaml)
}
Expand Down
63 changes: 59 additions & 4 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,18 +27,73 @@ interface AnyObject {
[key: string]: any;
}

export function deepAssign(target: AnyObject, source: AnyObject): AnyObject {
export function deepAssign(target: AnyObject, source: AnyObject, opts?: {arrayBehavior?: 'merge' | 'concat' | 'replace' | 'unique-key-objects'}): AnyObject {
if (typeof target !== 'object' || typeof source !== 'object') {
return source
}
const behavior = opts?.arrayBehavior

function handleUniqueKeyObjectsArray(target: any, source: any) {
source.forEach((s: any): any => {
let present = false
const sourceKey = Object.keys(s)[0]
target.forEach((t: any, index: number): any => {
const targetKey = Object.keys(t)[0]
if (targetKey === sourceKey) {
target[index][targetKey] = deepAssign(target[index][targetKey], s[sourceKey], {arrayBehavior: 'unique-key-objects'})
present = true
}
})
if (!present) {
target = target.concat(s)
}
})
return target
}

function handleArray(target: any, source: any) {
if (behavior === 'concat') {
return target.concat(source)
}
if (behavior === 'merge') {
return source.map((v: any, i: number) => {
return deepAssign(target[i] || {}, v, {arrayBehavior: 'merge'})
})
}
if (behavior === 'unique-key-objects') {
const isUniqueKeyObjectsArray = target.length > 1 && target.find((t: any) => {
return typeof t === 'object' && Object.keys(t).length === 1
})
if (isUniqueKeyObjectsArray) {
return handleUniqueKeyObjectsArray(target, source)
}
// if not uniq keys, fallback to merge behavior
return source.map((s: any, i: number) => {
return deepAssign(target[i], s, {arrayBehavior: 'merge'})
})
}
// replace target[key] (default behavior)
return source
}

Object.keys(source).forEach(k => {
if (typeof source[k] === 'object') {
if (source[k] && typeof source[k] === 'object') {
// source[key] value is truthy && of type object
// eslint-disable-next-line no-negated-condition
if (!target[k]) {
// doesn't exist, just assign it
// key doesn't exist, just assign it
target[k] = source[k]
} else if (Array.isArray(target[k])) {
// key is array, handle specially
target[k] = handleArray(target[k], source[k])
} else {
const newTk = deepAssign(target[k], source[k])
// key is obj, down the rabbithole again
const newTk = deepAssign(target[k], source[k], {arrayBehavior: behavior})
target[k] = newTk
}
} else {
// source[key] value is possible falsey || not of type object
// replace key with source
target[k] = source[k]
}
})
Expand Down
218 changes: 218 additions & 0 deletions test/util.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
const assert = require('assert')

import {deepAssign} from '../src/utils'

describe('deepAssign', function () {
let target = {}
beforeEach(() => {
// deepAssign mutates target
// so reassign it
target = {
greeting: 'Hello',
name: 'world',
nested: {
bool: true,
int: 123,
string: '!',
},
arrayUnchanged: [1, 2, 3],
array1: [
{
id: 0,
description: 'foo',
},
{
id: 1,
description: 'bar',
},
{
id: 2,
description: 'baz',
},
],
}
})

const source = {
name: 'leif',
nested: {
bool: false,
},
array1: [
{
id: 3,
description: 'qux',
},
],
}

it('should replace the array', function () {
const expected = {
greeting: 'Hello',
name: 'leif',
nested: {
bool: false,
int: 123,
string: '!',
},
arrayUnchanged: [1, 2, 3],
array1: [
{
id: 3,
description: 'qux',
},
],
}
const actual = deepAssign(target, source)
assert.deepStrictEqual(actual, expected)
})

it('should concat the array', function () {
const expected = {
greeting: 'Hello',
name: 'leif',
nested: {
bool: false,
int: 123,
string: '!',
},
arrayUnchanged: [1, 2, 3],
array1: [
{
id: 0,
description: 'foo',
},
{
id: 1,
description: 'bar',
},
{
id: 2,
description: 'baz',
},
{
id: 3,
description: 'qux',
},
],
}
const actual = deepAssign(target, source, {arrayBehavior: 'concat'})
assert.deepStrictEqual(actual, expected)
})

it('should merge the array on indexes', function () {
const source = {
name: 'leif',
nested: {
bool: false,
},
array1: [
{},
{description: 'qux'},
'mixed type',
{
id: 3,
description: 'qux',
},
],
}

const expected = {
greeting: 'Hello',
name: 'leif',
nested: {
bool: false,
int: 123,
string: '!',
},
arrayUnchanged: [1, 2, 3],
array1: [
{
id: 0,
description: 'foo',
},
{
id: 1,
description: 'qux',
},
'mixed type',
{
id: 3,
description: 'qux',
},
],
}
const actual = deepAssign(target, source, {arrayBehavior: 'merge'})
assert.deepStrictEqual(actual, expected)
})

it('should use unique-object-keys method for handling array', function () {
const target = {
workflows: [
{foo: {
id: 0,
desc: 'foo',
}},
{bar: {
id: 1,
desc: 'bar',
}},
{baz: {
id: 2,
desc: 'baz',
array: [0, 1, 2],
}},
],
}

const source = {
workflows: [
{
baz: {
id: 2,
desc: 'updated',
array: [3, 2, 1],
},
},
{
qux: {
id: 3,
desc: 'qux',
},
},
],
}

const expected = {
workflows: [
{
foo: {
id: 0,
desc: 'foo',
},
},
{
bar: {
id: 1,
desc: 'bar',
},
},
{
baz: {
id: 2,
desc: 'updated',
array: [3, 2, 1],
},
},
{
qux: {
id: 3,
desc: 'qux',
},
},
],
}
const actual = deepAssign(target, source, {arrayBehavior: 'unique-key-objects'})
assert.deepStrictEqual(actual, expected)
})
})

0 comments on commit 24456d2

Please sign in to comment.