UNPKG

objtools

Version:

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

596 lines (570 loc) 18.5 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'; const ObjectMask = objtools.ObjectMask; describe('ObjectMask', function() { describe('new ObjectMask()', function() { it('underscorizes wildcard arrays', function() { let orig = new ObjectMask({ foo: [ { baz: true } ] }); let expectedMask = { foo: { _: { baz: true } } }; expect(orig.mask).to.deep.equal(expectedMask); }); }); describe('isObjectMask()', function() { it('returns true for ObjectMasks', function() { expect(ObjectMask.isObjectMask(new ObjectMask({}))).to.be.true; }); it('returns false for non-ObjectMasks', function() { expect(ObjectMask.isObjectMask({ mask: {}, isObjectMask: true })).to.be.false; }); }); describe('addMasks()', function() { const mask1 = ({ str1: true, str2: true, num1: true, nul1: true, nul2: true, obj: { foo: true, bar: true }, arr: [ { str1: true } ] }); const mask2 = new ObjectMask({ str1: true, num2: true, nul2: true, obj: { _: true, foo: false }, arr: [ { str2: true } ] }); const result = ObjectMask.addMasks(mask1, mask2); it('makes a mask that matches what either mask matches', function() { const expected = { str1: true, str2: true, num1: true, num2: true, nul1: true, nul2: true, obj: { _: true }, arr: { _: { str1: true, str2: true } } }; expect(result.checkFields(expected)).to.be.true; }); }); describe('subtractMasks()', function() { const subtract = (a, b) => ObjectMask.subtractMasks(a, b).mask; const minuend = new ObjectMask({ str1: true, num1: true, nul1: true, nul2: true, obj: { _: true, baz: false }, obj1: { foo: true, bar: true }, obj3: { foo: { foo: true, bar: true } }, obj4: { _: true } }); const subtrahend = new ObjectMask({ str1: true, num2: true, nul2: true, obj: { quux: true }, obj1: { _: true, foo: false }, obj3: { _: { bar: false }, foo: { bar: true } }, obj4: { _: true } }); const difference = ObjectMask.subtractMasks(minuend, subtrahend); it('subtracts a mask', function() { const expected = { num1: true, nul1: true, obj: { _: true, baz: false, quux: false }, obj1: { foo: true }, obj3: { _: { _: true, bar: true }, foo: { foo: true } } }; expect(difference.toObject()).to.deep.equal(expected); }); it('subtracts primitive masks', function() { expect(subtract(true, true)).to.equal(false); expect(subtract(true, false)).to.equal(true); expect(subtract(false, true)).to.equal(false); expect(subtract(false, false)).to.equal(false); }); it('returns false when subtracting a scalar from false', function() { expect(subtract(false, { _: true, foo: false })).to.equal(false); }); it('no longer matches what the subtrahend matches', function() { const obj = { str1: 0, nul2: 2, obj: { quux: 3 }, obj1: { bar: 4 }, obj3: { foo: { bar: 6 } } }; const filtered = { obj: {}, obj1: {}, obj3: { foo: {} } }; expect(subtrahend.filterObject(obj)).to.deep.equal(obj, 'subtrahend doesnt match obj'); expect(minuend.filterObject(obj)).to.deep.equal(obj, 'minuend doesnt match obj'); expect(difference.filterObject(obj)).to.deep.equal(filtered, 'difference matches obj'); }); it('still matches what the subtrahend did not match', function() { const obj = { num1: 0, nul1: 1, obj: { foo: 2 }, obj1: { foo: 3 }, obj3: { foo: { foo: 5 } } }; const filtered = { obj: {}, obj1: {}, obj3: { foo: {} } }; expect(subtrahend.filterObject(obj)).to.deep.equal(filtered, 'subtrahend matches obj'); expect(minuend.filterObject(obj)).to.deep.equal(obj, 'minuend doesnt match obj'); expect(difference.filterObject(obj)).to.deep.equal(obj, 'difference doesnt match obj'); }); it('still doesnt match what the minuend did not match', function() { const obj = { foo: 0, baz: { quux: 1 } }; expect(minuend.filterObject(obj)).to.deep.equal({}); expect(difference.filterObject(obj)).to.deep.equal({}); }); it('throws on attempt to subtract with scalars', function() { expect(() => ObjectMask.subtractMasks(3, { _: { foo: true } })).to.throw(Error); expect(() => ObjectMask.subtractMasks({ _: { foo: true } }, 3)).to.throw(Error); }); }); describe('andMasks()', function() { const mask1 = new ObjectMask({ str1: true, str2: true, num1: true, nul1: true, nul2: true, obj: { foo: true, bar: true }, arr: [ { str1: true } ] }); const mask2 = new ObjectMask({ str1: true, num2: true, nul2: true, obj: { _: true, foo: false }, arr: [ { str2: true } ] }); it('makes a mask that matches all fields that both masks match', function() { const result = ObjectMask.andMasks(mask1, mask2); const expected = { str1: true, nul2: true, obj: { bar: true } }; expect(result.checkFields(expected)).to.be.true; }); }); describe('#subtractMask()', function() { const minuend = new ObjectMask({ str1: true, num1: true, nul1: true, nul2: true, obj: { _: true, baz: false }, obj1: { foo: true, bar: true }, obj3: { foo: { foo: true, bar: true } }, obj4: { _: true } }); const subtrahend = new ObjectMask({ str1: true, num2: true, nul2: true, obj: { quux: true }, obj1: { _: true, foo: false }, obj3: { _: { bar: false }, foo: { bar: true } }, obj4: { _: true } }); const difference = new ObjectMask(minuend.mask).subtractMask(subtrahend); it('subtracts a mask', function() { const expected = { num1: true, nul1: true, obj: { _: true, baz: false, quux: false }, obj1: { foo: true }, obj3: { _: { _: true, bar: true }, foo: { foo: true } } }; expect(difference.toObject()).to.deep.equal(expected); }); it('no longer matches what the subtrahend matches', function() { const obj = { str1: 0, nul2: 2, obj: { quux: 3 }, obj1: { bar: 4 }, obj3: { foo: { bar: 6 } } }; const filtered = { obj: {}, obj1: {}, obj3: { foo: {} } }; expect(subtrahend.filterObject(obj)).to.deep.equal(obj, 'subtrahend doesnt match obj'); expect(minuend.filterObject(obj)).to.deep.equal(obj, 'minuend doesnt match obj'); expect(difference.filterObject(obj)).to.deep.equal(filtered, 'difference matches obj'); }); it('still matches what the subtrahend did not match', function() { const obj = { num1: 0, nul1: 1, obj: { foo: 2 }, obj1: { foo: 3 }, obj3: { foo: { foo: 5 } } }; const filtered = { obj: {}, obj1: {}, obj3: { foo: {} } }; expect(subtrahend.filterObject(obj)).to.deep.equal(filtered, 'subtrahend matches obj'); expect(minuend.filterObject(obj)).to.deep.equal(obj, 'minuend doesnt match obj'); expect(difference.filterObject(obj)).to.deep.equal(obj, 'difference doesnt match obj'); }); it('still doesnt match what the minuend did not match', function() { const obj = { foo: 0, baz: { quux: 1 } }; expect(minuend.filterObject(obj)).to.deep.equal({}); expect(difference.filterObject(obj)).to.deep.equal({}); }); it('throws on attempt to subtract with scalars', function() { expect(() => ObjectMask.subtractMasks(3, { _: { foo: true } })).to.throw(Error); expect(() => ObjectMask.subtractMasks({ _: { foo: true } }, 3)).to.throw(Error); }); }); describe('#filterObject()', function() { let obj = { str1: 'string', str2: 'string2', num1: 1, num2: 2, nul1: null, nul2: null, undef: undefined, obj: { foo: 'test', bar: 'test2', baz: 'test3' }, arr: [ { str1: 'one', str2: 'two' }, { str1: 'three', str2: 'four' } ] }; it('gets the fields in an object the mask matches', function() { const mask = new ObjectMask({ str1: true, num1: true, nul1: true, obj: { bar: true, nonexist: true } }); const result = mask.filterObject(objtools.deepCopy(obj)); expect(result).to.deep.equal({ str1: 'string', num1: 1, nul1: null, obj: { bar: 'test2' } }); }); it('handles arrays and wildcards', function() { const mask = new ObjectMask({ obj: { _: true, bar: false }, arr: [ { str2: true } ] }); const result = mask.filterObject(objtools.deepCopy(obj)); expect(result).to.deep.equal({ obj: { foo: 'test', baz: 'test3' }, arr: [ { str2: 'two' }, { str2: 'four' } ] }); }); }); describe('#getSubMask()', function() { it('gets a submask', function() { const mask = new ObjectMask({ foo: { bar: { baz: true } } }); const result = mask.getSubMask('foo').toObject(); expect(result).to.deep.equal({ bar: { baz: true } }); }); it('handles wildcards', function() { const mask = new ObjectMask({ obj: { _: true, foo: false } }); expect(mask.getSubMask('obj.bar').mask).to.be.true; expect(mask.getSubMask('obj.foo').mask).to.be.false; }); }); describe('#checkPath()', function() { const mask = new ObjectMask({ obj: { _: true, foo: false }, arr: [ { str2: true } ] }); it('checks if the mask matches the path', function() { expect(mask.checkPath('arr.8.str1')).to.be.false; expect(mask.checkPath('arr.8.str2')).to.be.true; }); it('handles wildcards', function() { expect(mask.checkPath('obj.bar.foo')).to.be.true; expect(mask.checkPath('obj.foo.foo')).to.be.false; }); }); describe('#invertMask()', function() { it('inverts a flat mask using a wildcard', function() { const mask = new ObjectMask({ foo: true }); const result = ObjectMask.invertMask(mask); const expected = { _: true, foo: false }; expect(result.mask).to.deep.equal(expected); }); it('handles wildcards', function() { const mask = new ObjectMask({ _: true, foo: false }); const result = ObjectMask.invertMask(mask); const expected = { foo: true }; expect(result.mask).to.deep.equal(expected); }); it('recurses to submasks', function() { const mask = new ObjectMask({ foo: { _: true, bar: false } }); const result = ObjectMask.invertMask(mask); const expected = { _: true, foo: { bar: true } }; expect(result.mask).to.deep.equal(expected); }); }); describe('#validate()', function() { let mask1 = { str1: true, str2: true, num1: true, nul1: true, nul2: true, obj: { foo: true, bar: true }, arr: [ { str1: true } ] }; let mask2 = { str1: true, num2: true, nul2: true, obj: { _: true, foo: false }, arr: [ { str2: true } ] }; it('returns true iff the mask is valid', function() { expect(new ObjectMask(mask1).validate()).to.be.true; expect(new ObjectMask(mask2).validate()).to.be.true; expect(new ObjectMask({ foo: new Date() }).validate()).to.be.false; }); }); describe('#getMaskedOutFields()', function() { const obj = { str1: 'string', str2: 'string2', num1: 1, num2: 2, nul1: null, nul2: null, undef: undefined, obj: { foo: 'test', bar: 'test2', baz: 'test3' }, arr: [ { str1: 'one', str2: 'two' }, { str1: 'three', str2: 'four' } ] }; const mask = new ObjectMask({ str1: true, str2: true, num1: true, nul1: true, nul2: true, obj: { foo: true, bar: true }, arr: [ { str1: true } ] }); it('returns a list of fields disallowed by the mask', function() { const expected = [ 'num2', 'undef', 'obj.baz', 'arr.0.str2', 'arr.1.str2' ]; expect(mask.getMaskedOutFields(obj).sort()).to.deep.equal(expected.sort()); }); }); describe('#filterDottedObject()', function() { const obj = { str1: 'string', str2: 'string2', num1: 1, num2: 2, nul1: null, nul2: null, undef: undefined, obj: { foo: 'test', bar: 'test2', baz: 'test3' }, arr: [ { str1: 'one', str2: 'two' }, { str1: 'three', str2: 'four' } ] }; const mask = new ObjectMask({ str1: true, num2: true, nul2: true, obj: { _: true, foo: false }, arr: [ { str2: true } ] }); it('filters disallowed fields from a dotted object', function() { const filtered = mask.filterDottedObject(objtools.collapseToDotted(obj)); const expected = { 'str1': 'string', 'num2': 2, 'nul2': null, 'obj.bar': 'test2', 'obj.baz': 'test3', 'arr.0.str2': 'two', 'arr.1.str2': 'four' }; expect(filtered).to.deep.equal(expected); }); }); describe('#getDottedMaskedOutFields()', function() { let obj = { str1: 'string', str2: 'string2', num1: 1, num2: 2, nul1: null, nul2: null, undef: undefined, obj: { foo: 'test', bar: 'test2', baz: 'test3' }, arr: [ { str1: 'one', str2: 'two' }, { str1: 'three', str2: 'four' } ] }; let mask = new ObjectMask({ str1: true, str2: true, num1: true, nul1: true, nul2: true, obj: { foo: true, bar: true }, arr: [ { str1: true } ] }); const fields = mask.getDottedMaskedOutFields(objtools.collapseToDotted(obj)); it('gets a list of fields in a dotted object disallowed by the mask', function() { const expected = [ 'num2', 'undef', 'obj.baz', 'arr.0.str2', 'arr.1.str2' ]; expect(fields).to.deep.equal(expected); }); }); describe('#checkFields()', function() { const mask = new ObjectMask({ str1: true, str2: true, num1: true, nul1: true, nul2: true, obj: { foo: true, bar: true }, arr: [ { str1: true } ] }); it('returns a predicate for whether a dotted field is allowed by a mask', function() { expect(mask.checkFields({ str1: 5 })).to.be.true; expect(mask.checkFields({ num2: 5 })).to.be.false; expect(mask.checkFields({ obj: { foo: 5 } })).to.be.true; expect(mask.checkFields({ obj: { baz: 5 } })).to.be.false; }); }); describe('#checkDottedFields()', function() { const mask = new ObjectMask({ str1: true, str2: true, num1: true, nul1: true, nul2: true, obj: { foo: true, bar: true }, arr: [ { str1: true } ] }); it('returns a predicate for whether a dotted object matches the mask', function() { expect(mask.checkDottedFields({ 'obj.foo': 5 })).to.be.true; expect(mask.checkDottedFields({ 'obj.baz': 5 })).to.be.false; }); }); describe('#createMaskFromFieldList()', function() { const fields = [ 'foo', 'bar.baz', 'bar.baz.biz' ]; const result = ObjectMask.createMaskFromFieldList(fields).toObject(); it('makes a mask that matches the fields in the fieldlist', function() { const expected = { foo: true, bar: { baz: true } }; expect(result).to.deep.equal(expected); }); }); describe('#createFilterFunc()', function() { const obj = { str1: 'string', str2: 'string2', num1: 1, num2: 2, nul1: null, nul2: null, undef: undefined, obj: { foo: 'test', bar: 'test2', baz: 'test3' }, arr: [ { str1: 'one', str2: 'two' }, { str1: 'three', str2: 'four' } ] }; const mask = new ObjectMask({ str1: true, str2: true, num1: true, nul1: true, nul2: true, obj: { foo: true, bar: true }, arr: [ { str1: true } ] }); const func = mask.createFilterFunc(); it('makes a predicate for whether an object matches a mask', function() { const expected = { str1: 'string', str2: 'string2', num1: 1, nul1: null, nul2: null, obj: { foo: 'test', bar: 'test2' }, arr: [ { str1: 'one' }, { str1: 'three' } ] }; expect(func(obj)).to.deep.equal(expected); }); }); describe('#addField()', function() { it('does not affect masks that already match', function() { const orig = new ObjectMask({ foo: true }); const expected = _.cloneDeep(orig); expect(orig.addField('foo.bar')).to.deep.equal(expected); }); it('recurses to subfields', function() { const orig = new ObjectMask({ foo: false, baz: true }); const expected = new ObjectMask({ foo: { bar: true }, baz: true }); expect(orig.addField('foo.bar')).to.deep.equal(expected); }); it('prunes to become more general', function() { const orig = new ObjectMask({ foo: { bar: true } }); const expected = new ObjectMask({ foo: true }); expect(orig.addField('foo')).to.deep.equal(expected); }); it('does not become more restrictive', function() { const orig = new ObjectMask({ _: true }); const shouldPass = { foo: { baz: 1 } }; expect(orig.checkFields(shouldPass)).to.be.true; expect(orig.addField('foo.bar').checkFields(shouldPass)).to.be.true; }); }); describe('#removeField()', function() { const shouldPass = { foo: { bar: { foobar: true } } }; const shouldAlsoPass = { foo: { biz: { baz: true } } }; const shouldFail = { foo: { bar: { baz: true } } }; const expected = new ObjectMask({ foo: { bar: { baz: false, foobar: true }, _: { baz: true, foobar: true } } }); it('branches wildcards', function() { let orig = new ObjectMask({ foo: { _: { baz: true, foobar: true } } }); expect(orig.removeField('foo.bar.baz').checkFields(shouldPass), 'post: shouldPass').to.be.true; expect(orig.removeField('foo.bar.baz').checkFields(shouldAlsoPass), 'post: shouldAlsoPass').to.be.true; expect(orig.removeField('foo.bar.baz').checkFields(shouldFail), 'post: shouldFail').to.be.false; expect(orig.removeField('foo.bar.baz')).to.deep.equal(expected); }); it('doesnt remove other fields', function() { let orig = new ObjectMask({ foo: { _: { baz: true, foobar: true } } }); expect(orig.checkFields(shouldPass), 'pre: shouldPass').to.be.true; expect(orig.checkFields(shouldAlsoPass), 'pre: shouldAlsoPass').to.be.true; expect(orig.checkFields(shouldFail), 'pre: shouldFail').to.be.true; expect(orig.removeField('foo.bar.baz').checkFields(shouldPass), 'post: shouldPass').to.be.true; expect(orig.removeField('foo.bar.baz').checkFields(shouldAlsoPass), 'post: shouldAlsoPass').to.be.true; expect(orig.removeField('foo.bar.baz').checkFields(shouldFail), 'post: shouldFail').to.be.false; }); it('doesnt match new fields', function() { let orig = new ObjectMask({ _: { bar: { baz: true } } }); let shouldFail = { foo: { bar: { foobar: true } } }; expect(orig.checkFields(shouldFail)).to.be.false; expect(orig.removeField('foo.bar.baz').checkFields(shouldFail)).to.be.false; }); it('throws on attempt to remove wildcard', function() { let orig = new ObjectMask({ _: [ true ] }); expect(() => orig.removeField('_')).to.throw(); expect(() => orig.removeField('foo._')).to.throw(Error); }); }); });