UNPKG

objtools

Version:

Various utility functions for working with object, including object merging, inheritance, deep copying, etc.

701 lines 29.6 kB
// Copyright 2016 Zipscene, LLC // Licensed under the Apache License, Version 2.0 // http://www.apache.org/licenses/LICENSE-2.0 import { expect } from 'chai'; import _ from 'lodash'; import * as objtools from '../lib/index.js'; class TestClass { testMethod() { return true; } } describe('Base Functions', function () { describe('isScalar()', function () { it('should return true for scalar values', function () { expect(objtools.isScalar(true)).to.be.true; expect(objtools.isScalar(false)).to.be.true; expect(objtools.isScalar(new Date())).to.be.true; expect(objtools.isScalar(123)).to.be.true; expect(objtools.isScalar(null)).to.be.true; expect(objtools.isScalar(undefined)).to.be.true; expect(objtools.isScalar(function () { })).to.be.true; }); it('should return false for non-scalar values', function () { expect(objtools.isScalar({})).to.be.false; expect(objtools.isScalar([])).to.be.false; expect(objtools.isScalar(new Error())).to.be.false; }); }); describe('scalarEquals', function () { const date1 = new Date('2014-01-01T00:00:00Z'); const date2 = new Date('2014-01-01T00:00:00Z'); const date3 = new Date('2014-01-01T00:00:01Z'); const func = function () { }; const obj = {}; it('should handle dates', function () { expect(objtools.scalarEquals(date1, date2)).to.be.true; expect(objtools.scalarEquals(date2, date3)).to.be.false; }); it('should handle other types', function () { expect(objtools.scalarEquals(2, 2)).to.be.true; expect(objtools.scalarEquals(true, true)).to.be.true; expect(objtools.scalarEquals(null, null)).to.be.true; expect(objtools.scalarEquals(undefined, undefined)).to.be.true; expect(objtools.scalarEquals(0, null)).to.be.false; expect(objtools.scalarEquals(obj, obj)).to.be.true; expect(objtools.scalarEquals(func, func)).to.be.true; expect(objtools.scalarEquals(obj, func)).to.be.false; expect(objtools.scalarEquals({}, {})).to.be.false; }); }); describe('deepEquals()', function () { const date1 = new Date('2014-01-01T00:00:00Z'); const date2 = new Date('2014-01-01T00:00:00Z'); const date3 = new Date('2014-01-01T00:00:01Z'); const obj1 = { foo: { bar: 'baz', biz: [1, 2] } }; const obj2 = { foo: { bar: 'baz', biz: [1, 2] } }; const obj3 = { foo: { bar: 'biz' }, biz: [1, 2] }; let obj4 = { foo: { bar: 'biz' }, biz: [1] }; it('should handle dates correctly', function () { expect(objtools.deepEquals(date1, date2)).to.be.true; expect(objtools.deepEquals(date2, date3)).to.be.false; expect(objtools.deepEquals({ d: date1 }, { d: date2 })).to.be.true; }); it('should handle objects correctly', function () { expect(objtools.deepEquals(obj1, obj2)).to.be.true; expect(objtools.deepEquals(obj2, obj3)).to.be.false; expect(objtools.deepEquals(obj3, obj4)).to.be.false; }); it('should not coerce types', function () { expect(objtools.deepEquals({ a: null }, { a: null })).to.be.true; expect(objtools.deepEquals({ a: undefined }, { a: undefined })).to.be.true; expect(objtools.deepEquals({ a: null }, { a: undefined })).to.be.false; expect(objtools.deepEquals({ a: 0 }, { a: null })).to.be.false; }); }); describe('deepCopy()', function () { const obj1 = { foo: 'bar', fuzz: 123, biz: { dat: new Date('2014-01-01T00:00:00Z'), n: null, u: undefined }, arr: [1, 2] }; const obj2 = { foo: new TestClass() }; it('should correctly copy objects', function () { const copy = objtools.deepCopy(obj1); expect(copy).to.deep.equal(obj1); expect(null).to.not.deep.equal(undefined); // make sure chai does what we want it to }); it('should not maintain references to objects', function () { const copy = objtools.deepCopy(obj1); expect(copy).to.deep.equal(obj1); copy.biz.dat = 123; expect(copy).to.not.deep.equal(obj1); }); it('should copy non-primitive objects', function () { const copy = objtools.deepCopy(obj2); expect(copy.foo).to.equal(obj2.foo); }); }); describe('collapseToDotted()', function () { const obj1 = { foo: 123, bar: { biz: 12, baz: { buz: 1 } }, arr: [1, 2, { foo: 3 }] }; it('should correctly collapse objects to dotted form', function () { const dotted = objtools.collapseToDotted(obj1); expect(dotted).to.deep.equal({ 'foo': 123, 'bar.biz': 12, 'bar.baz.buz': 1, 'arr.0': 1, 'arr.1': 2, 'arr.2.foo': 3 }); }); it('should obey includeRedundantLevels', function () { const dotted = objtools.collapseToDotted(obj1, true); expect(dotted).to.deep.equal({ 'foo': 123, 'bar': obj1.bar, 'bar.biz': 12, 'bar.baz': obj1.bar.baz, 'bar.baz.buz': 1, 'arr': obj1.arr, 'arr.0': 1, 'arr.1': 2, 'arr.2.foo': 3, 'arr.2': obj1.arr[2] }); // check for referential equality expect(dotted.bar).to.equal(obj1.bar); expect(dotted.bar.baz).to.equal(obj1.bar.baz); }); it('should obey stopAtArrays', function () { const dotted = objtools.collapseToDotted(obj1, false, true); expect(dotted).to.deep.equal({ 'foo': 123, 'bar.biz': 12, 'bar.baz.buz': 1, 'arr': obj1.arr }); expect(dotted.arr).to.equal(obj1.arr); }); }); describe('matchObject()', function () { // it's not like an ObjectMask const obj = { foo: 'foo', bar: { biz: 12 }, zip: [4, 5] }; it('returns true if it matches', function () { expect(objtools.matchObject(obj, { foo: 'foo' })).to.be.true; expect(objtools.matchObject(obj, { bar: { biz: 12 } })).to.be.true; expect(objtools.matchObject(obj, { zip: [4] })).to.be.true; }); it('returns false if it doesnt match', function () { expect(objtools.matchObject(obj, { foo: true })).to.be.false; expect(objtools.matchObject(obj, { bar: { _: 12 } })).to.be.false; expect(objtools.matchObject(obj, { zip: [4, 5, 6] })).to.be.false; }); }); describe('matchDottedObject()', function () { const obj = { foo: 'foo', bar: { biz: 12 }, zip: [4, 5] }; const dotted1 = { 'foo': 'foo', 'bar': { biz: 12 }, 'zip': [4, 5] }; const dotted2 = { 'foo': 'foo', 'bar': { biz: 12 }, 'zip': [4, 2] }; it('returns true if it matches', function () { expect(objtools.matchDottedObject(obj, dotted1)).to.be.true; }); it('returns false if it doesnt match', function () { expect(objtools.matchDottedObject(obj, dotted2)).to.be.false; }); }); describe('syncObject()', function () { const fromObj = { foo: 'bar', baz: { qux: [ { zip: 'zap', bam: new Date('2014-01-01T00:00:00Z') }, { bip: 'boop' } ] }, foop: { flap: 'flip' }, qax: new Date('2014-01-02T00:00:00Z') }; it('should copy an object to the destination', function () { let toObj = { foo: 'bip', zap: 'zip', qux: { boom: 123 }, foop: { flap: 'flop' }, qax: new Date('2014-01-01T00:00:00Z') }; const origFoop = toObj.foop; const expected = objtools.deepCopy(fromObj); objtools.syncObject(toObj, fromObj); expect(toObj).to.deep.equal(fromObj); // make sure it didn't modify fromObj expect(fromObj).to.deep.equal(expected); // make sure it didn't change the internal object reference expect(toObj.foop).to.equal(origFoop); }); it('should skip fields when the onField hook returns false', function () { let toObj = { foo: 'bip', zap: 'zip', baz: { qux: 123 }, qux: { boom: 123 }, foop: { flap: 'flop' } }; objtools.syncObject(toObj, fromObj, { onField: (field) => field !== 'baz.qux' }); expect(toObj).to.deep.equal({ foo: 'bar', baz: { qux: 123 }, foop: { flap: 'flip' }, qax: new Date('2014-01-02T00:00:00Z') }); }); it('should call onChange for changed fields', function () { let toObj = { foo: 'bip', zoop: 'zip', baz: { qux: 123 }, qux: { boom: 123 }, foop: { flap: 'flop' }, zap: 4 }; let changed = []; objtools.syncObject(toObj, fromObj, { onChange: (field) => changed.push(field) }); const expected = ['foo', 'zoop', 'baz.qux', 'qax', 'qux', 'foop.flap', 'zap']; expect(toObj).to.deep.equal(fromObj); expect(changed.sort()).to.deep.equal(expected.sort()); }); it('should work with arrays and nested arrays', function () { let fromArrObj = { a: [[1, 2, 3], [44]], b: [1, 2, 3, 4, 5], c: [10], d: [{ stuff: 'good', bar: 'foo' }] }; let toArrObj = { a: [[11, 2, 3, 4], [44], [55]], b: [1, 2, 3], c: [10, 11], d: [{ stuff: 'just ok', foo: 'bar' }] }; objtools.syncObject(toArrObj, fromArrObj); expect(toArrObj).to.deep.equal({ a: [[1, 2, 3], [44]], b: [1, 2, 3, 4, 5], c: [10], d: [{ stuff: 'good', bar: 'foo' }] }); }); }); describe('path functions', function () { let obj1 = { foo: 'bar', baz: { biz: 'buz', arr: [1, 2, { zip: 3 }], arr2: [{ zip: 4 }] } }; it('getPath should fetch basic object paths', function () { expect(objtools.getPath(obj1, 'foo')).equals(obj1.foo); expect(objtools.getPath(obj1, 'baz')).equals(obj1.baz); expect(objtools.getPath(obj1, 'baz.biz')).equals(obj1.baz.biz); expect(objtools.getPath(obj1, 'baz.arr2.zip')).equals(undefined); expect(objtools.getPath(obj1, 'baz.arr.1')).equals(2); expect(objtools.getPath(obj1, 'baz.arr.2.zip')).equals(3); }); it('getPath should obey allowSkipArrays', function () { expect(objtools.getPath(obj1, 'foo', true)).equals(obj1.foo); expect(objtools.getPath(obj1, 'baz', true)).equals(obj1.baz); expect(objtools.getPath(obj1, 'baz.biz', true)).equals(obj1.baz.biz); expect(objtools.getPath(obj1, 'baz.arr2.zip', true)).equals(4); expect(objtools.getPath(obj1, 'baz.arr.1', true)).equals(2); expect(objtools.getPath(obj1, 'baz.arr.2.zip', true)).equals(3); }); it('getPath should handle root path', function () { expect(objtools.getPath(obj1, null)).to.deep.equal(obj1); }); it('setPath should set various paths', function () { objtools.setPath(obj1, 'foo', 'biz'); expect(obj1.foo).to.equal('biz'); objtools.setPath(obj1, 'baz.arr.1', 8); expect(obj1.baz.arr[1]).to.equal(8); }); it('setPath should create parent objects as necessary', function () { objtools.setPath(obj1, 'bar.biz.baz.buz', 10); // @ts-ignore expect(obj1.bar).to.deep.equal({ biz: { baz: { buz: 10 } } }); }); it('setPath should overwrite parent object on conflicting type', function () { objtools.setPath(obj1, 'baz.arr.1.buz', 11); expect(obj1.baz.arr[1]).to.deep.equal({ buz: 11 }); }); it('deletePath should delete paths', function () { objtools.deletePath(obj1, 'baz.arr'); expect(obj1.baz.arr).to.equal(undefined); }); }); describe('merge()', function () { const falsey = ['', 0, false, NaN, null, undefined]; it('should pass thru falsey `object` values', function () { const actual = _.map(falsey, value => objtools.merge(value)); expect(actual).to.deep.equal(falsey); }); it('should not error when `object` is nullish and source objects are provided', function () { expect(objtools.merge(null, { a: 1 })).to.deep.equal({ a: 1 }); expect(objtools.merge(undefined, { a: 1 })).to.deep.equal({ a: 1 }); }); it('should work as an iteratee for methods like `_.reduce`', function () { let array = [{ 'a': 1 }, { 'b': 2 }, { 'c': 3 }]; let expected = { 'a': 1, 'b': 2, 'c': 3 }; let actual = _.reduce(array, objtools.merge, { 'a': 0 }); expect(actual).to.deep.equal(expected); }); it('should provide the correct `customizer` arguments', function () { let object = { 'a': 1 }; let source = { 'a': 2 }; let args, expected = [1, 2, 'a', _.cloneDeep(object), _.cloneDeep(source)]; objtools.merge(_.cloneDeep(object), _.cloneDeep(source), function () { args = _.toArray(_.cloneDeep(arguments)); }); expect(args).to.deep.equal(expected, 'primitive property values'); args = null; object = { 'a': 1 }; source = { 'b': 2 }; expected = [undefined, 2, 'b', object, source]; objtools.merge(_.cloneDeep(object), _.cloneDeep(source), function () { args = _.toArray(_.cloneDeep(arguments)); }); expect(args).to.deep.equal(expected, 'missing destination property'); args = []; let objectValue = [1, 2]; let sourceValue = { 'b': 2 }; object = { 'a': objectValue }; source = { 'a': sourceValue }; expected = [ [objectValue, sourceValue, 'a', object, source], // note: this differs from the lodash test bc that test is wrong [undefined, 2, 'b', objectValue, sourceValue] ]; objtools.merge(_.cloneDeep(object), _.cloneDeep(source), function () { args.push(_.toArray(_.cloneDeep(arguments))); }); expect(args).to.deep.equal(expected, 'non-primitive property values'); }); it('should not assign the `customizer` result if it is the same as the destination value', function () { _.each(['a', ['a'], { 'a': 1 }, NaN], function (value) { let object = {}; let pass = true; Object.defineProperty(object, 'a', { 'get': _.constant(value), 'set': function () { pass = false; } }); objtools.merge(object, { 'a': value }, _.identity); expect(pass).to.be.true; }); }); it('should merge `source` into the destination object', function () { const names = { 'characters': [{ 'name': 'barney' }, { 'name': 'fred' }] }; const ages = { 'characters': [{ 'age': 36 }, { 'age': 40 }] }; const heights = { 'characters': [{ 'height': '5\'4"' }, { 'height': '5\'5"' }] }; const expected = { 'characters': [ { 'name': 'barney', 'age': 36, 'height': '5\'4"' }, { 'name': 'fred', 'age': 40, 'height': '5\'5"' } ] }; expect(objtools.merge(names, ages, heights)).to.deep.equal(expected); }); it('should work with four arguments', function () { const expected = { 'a': 4 }; const actual = objtools.merge({ 'a': 1 }, { 'a': 2 }, { 'a': 3 }, expected); expect(actual).to.deep.equal(expected); }); it('should assign `null` values', function () { const actual = objtools.merge({ 'a': 1 }, { 'a': null }); expect(actual.a).to.equal(null); }); it('should not assign `undefined` values', function () { const actual = objtools.merge({ 'a': 1 }, { 'a': undefined, 'b': undefined }); expect(actual).to.deep.equal({ 'a': 1 }); }); it('should work with a function `object` value', function () { function Foo() { } const source = { 'a': 1 }; expect(objtools.merge(Foo, source) === Foo); // @ts-ignore expect(Foo.a === 1); }); it('should override primitive `object` values', function () { const values = [true, 1, '1']; const actual = _.map(values, value => objtools.merge(value, { 'a': 1 })); expect(actual).to.deep.equal([{ a: 1 }, { a: 1 }, { a: 1 }]); }); it('should handle merging if `customizer` returns `undefined`', function () { const actual = objtools.merge({ 'a': { 'b': [1, 1] } }, { 'a': { 'b': [0] } }, _.noop); expect(actual).to.deep.equal({ 'a': { 'b': [0, 1] } }); expect(objtools.merge([], [undefined], _.identity)).to.deep.equal([undefined]); }); it('should defer to `customizer` when it returns a value other than `undefined`', function () { const customizer = (a, b) => (_.isArray(a) ? a.concat(b) : undefined); const actual = objtools.merge({ 'a': { 'b': [0, 1] } }, { 'a': { 'b': [2] } }, customizer); expect(actual).to.deep.equal({ 'a': { 'b': [0, 1, 2] } }); }); it('handles deep heterogeneous types', function () { let obj = { a: { b: ['c'], d: 'e', f: { g: 'h' }, i: 'jk', l: ['o', 'l'] } }; const source = { a: { b: 'c', d: ['e'], f: 'gh', i: { j: 'k' }, l: { o: 'l' } } }; const expected = { a: { b: 'c', d: ['e'], f: 'gh', i: { j: 'k' }, l: _.extend(['o', 'l'], { o: 'l' }) } }; expect(objtools.merge(obj, source)).to.deep.equal(expected); }); it('copies constructors', function () { let obj = { foo: String }; let result = objtools.merge({}, obj); expect(result.foo).to.equal(String); }); it('copies non-plain objects', function () { function TestClass() { } let obj = { foo: new TestClass() }; let result = objtools.merge({}, obj); expect(result.foo).to.equal(obj.foo); }); }); describe('getDuplicates()', function () { const arr = ['a', 'b', 'a', 'c', 'c']; it('gets the duplicates in an array of strings', function () { const result = objtools.getDuplicates(arr); const expected = ['a', 'c']; expect(result).to.contain.members(expected); expect(result.length).to.equal(expected.length); }); }); describe('diffObjects()', function () { const a = { a: 'b', c: 'd', e: 'f', g: 'h', i: { j: 'k' }, l: { m: 'n', o: { p: 'q' } } // value is a collection with some overlapping fields across objects }; const b = { a: 'b', c: 1, e: 'f', g: { h: true }, i: { k: 'j' }, l: { m: 'nop' } }; const c = { a: 'b', c: false, i: { jk: true }, l: { m: 'no', p: 'q' } }; const aScalar = 'scalar'; it('diffs two objects', function () { const result = objtools.diffObjects(a, b); const expected = { c: ['d', 1], g: ['h', { h: true }], i: [{ j: 'k' }, { k: 'j' }], l: { m: ['n', 'nop'], o: [{ p: 'q' }, null] } }; expect(result).to.deep.equal(expected); }); it('diffs n objects', function () { const result = objtools.diffObjects(a, b, c); const expected = { c: ['d', 1, false], e: ['f', 'f', null], g: ['h', { h: true }, null], i: [{ j: 'k' }, { k: 'j' }, { jk: true }], l: { m: ['n', 'nop', 'no'], o: [{ p: 'q' }, null, null], p: [null, null, 'q'] } }; expect(result).to.deep.equal(expected); }); it('handles scalars', function () { const result = objtools.diffObjects(a, b, aScalar); const expected = _.extend([null, null, aScalar], { a: ['b', 'b', null], c: ['d', 1, null], e: ['f', 'f', null], g: ['h', { h: true }, null], i: [{ j: 'k' }, { k: 'j' }, null], l: { m: ['n', 'nop', null], o: [{ p: 'q' }, null, null] } }); expect(result).to.deep.equal(expected); }); }); describe('dottedDiff()', function () { const obj1 = { a: { b: 'c', d: { e: 'f' } }, d: 'efg' }; const obj2 = { a: { b: 'c', d: true }, d: new Date('2015-01-01'), f: 'g' }; const aScalar = 'scalar'; const anotherScalar = new Date('2015-01-01'); const arr1 = [obj1.a, obj2.a, aScalar]; const arr2 = [obj1.d, obj2.d, aScalar]; it('diffs two objects', function () { const result = objtools.dottedDiff(obj1, obj2); const expected = ['a.d', 'd', 'f']; expect(result).to.contain.members(expected); expect(result.length).to.equal(expected.length); }); it('diffs two arrays', function () { const result = objtools.dottedDiff(arr1, arr2); const expected = ['0', '1']; expect(result).to.contain.members(expected); expect(result.length).to.equal(expected.length); }); it('diffs an object and an array', function () { const result = objtools.dottedDiff(obj1, arr1); const expected = _.union(_.keys(obj1), _.keys(arr1)); expect(result).to.contain.members(expected); expect(result.length).to.equal(expected.length); }); it('diffs an object and a scalar', function () { const result = objtools.dottedDiff(obj1, aScalar); const expected = ['']; expect(result).to.contain.members(expected); expect(result.length).to.equal(expected.length); }); it('diffs a scalar and an object', function () { const result = objtools.dottedDiff(aScalar, obj1); const expected = ['']; expect(result).to.contain.members(expected); expect(result.length).to.equal(expected.length); }); it('handles dates', function () { const diffDates = objtools.dottedDiff({ foo: new Date() }, { foo: new Date(0) }); const sameDates = objtools.dottedDiff({ foo: new Date(0) }, { foo: new Date(0) }); expect(diffDates).to.deep.equal(['foo']); expect(sameDates).to.deep.equal([]); }); it('diffs two scalars', function () { expect(objtools.dottedDiff(aScalar, anotherScalar)).to.deep.equal(['']); }); it('handles deep equal values', function () { expect(objtools.dottedDiff(obj1, _.cloneDeep(obj1))).to.deep.equal([]); expect(objtools.dottedDiff(aScalar, aScalar)).to.deep.equal([]); }); }); describe('objectHash()', function () { const obj1a = { a: 1, b: '2', c: ['xyz', null, null], d: { asdf: 'jkl;' } }; const obj1b = { b: '2', c: ['xyz', null, null], d: { asdf: 'jkl;' }, a: 1 }; const obj2a = { b: '2', c: [null, 'xyz', null], d: { asdf: 'jkl;' }, a: 1 }; const prim1 = 12345; const prim2 = false; it('returns a hash', function () { let hash = objtools.objectHash(obj1a); expect(hash).to.be.a('string'); }); it('hashes primitives', function () { let hash1 = objtools.objectHash(prim1); expect(hash1).to.be.a('string'); let hash2 = objtools.objectHash(prim2); expect(hash2).to.be.a('string'); }); it('hashes are consistent', function () { let hash1 = objtools.objectHash(obj1a); let hash2 = objtools.objectHash(obj1b); expect(hash1).to.equal(hash2); }); it('hashes for different objects do not conflict', function () { let hash1 = objtools.objectHash(obj1a); let hash2 = objtools.objectHash(obj1b); let hash3 = objtools.objectHash(obj2a); expect(hash1).to.not.equal(hash3); expect(hash2).to.not.equal(hash3); }); }); describe('sanitizeDate()', function () { it('should convert a number of miliseconds to a Date instance', () => { let date = Date.now(); let sanitized = objtools.sanitizeDate(date); expect(sanitized).to.be.an.instanceof(Date); expect(sanitized.getTime()).to.equal(date); }); it('should concert a date string to a Date instance', () => { let date = new Date(); let sanitized = objtools.sanitizeDate(date.toISOString()); expect(sanitized).to.be.an.instanceof(Date); expect(sanitized.getTime()).to.equal(date.getTime()); }); it('should return the same object if a date instance is passed in', () => { let date = new Date(); let sanitized = objtools.sanitizeDate(date); expect(sanitized).to.be.an.instanceof(Date); expect(sanitized).to.deep.equal(date); }); it('should flatten object with a field `date`', () => { let date = { date: new Date() }; let sanitized = objtools.sanitizeDate(date); expect(sanitized).to.be.an.instanceof(Date); expect(sanitized).to.deep.equal(date.date); }); }); describe('isPlainObject()', function () { function TestConstructor() { } const values = { emptyObject: {}, 'null': null, plainObject: { foo: 'bar' }, 'function': function () { }, nativeFunction: String, date: new Date(), 'undefined': undefined, 'false': false, string: 'foo', 'true': true, classObject: new TestConstructor(), objectCreate: Object.create(null), array: [], jsonDecoded: JSON.parse('{"foo":"bar"}') }; _.forEach(values, function (value, key) { it('should handle ' + key, function () { expect(objtools.isPlainObject(value)).to.equal(_.isPlainObject(value)); }); }); }); describe('isEmpty()', function () { const values = { emptyObject: {}, 'null': null, plainObject: { foo: 'bar' }, 'function': function () { }, date: new Date(), 'undefined': undefined, string: 'foo', emptyString: '', array: ['foo'], emptyArray: [], jsonDecoded: JSON.parse('{"foo":"bar"}') }; _.forEach(values, function (value, key) { it('should handle ' + key, function () { expect(objtools.isEmpty(value)).to.equal(_.isEmpty(value)); }); }); }); }); //# sourceMappingURL=base-functions.js.map