import assert from 'assert' import { get, set, del, getKeys, walkPath, pathToString } from '../src/obj-nested-prop.js' import { check } from '@uci-utils/type' const date = new Date(Date.now()) console.log('run:', date.getMinutes(), ':', date.getSeconds()) const obj = { one: { two: { three: 'test', four: ['el0', 'el1'] } } } describe('get', function () { it('Should get value deep in object', function () { assert.strictEqual(get(obj, 'one.two.three'), 'test') }) it('Should return undefined if path is not in object', function () { assert.strictEqual(get(obj, 'one.seven.three'), undefined) }) it('Should get value from array deep in object', function () { assert.strictEqual(get(obj, 'one.two.four.1'), 'el1') }) }) describe('del', function () { it('deep property can be deleted', function () { del(obj, 'one.two.three') assert.deepStrictEqual(obj, { one: { two: { four: ['el0', 'el1'] } } }) }) }) describe('unsafe properties', () => { it('should not allow setting constructor', () => { // assert.deepStrictEqual(set({}, 'a.constructor.b', 'c'), {}) assert.throws(() => set({}, 'a.constructor', 'c')) // assert.throws(() => set({}, 'constructor', 'c')) }) it('should not allow setting prototype', () => { assert.throws(() => set({}, 'a.prototype.b', 'c')) assert.throws(() => set({}, 'a.prototype', 'c')) assert.throws(() => set({}, 'prototype', 'c')) }) it('should not allow setting __proto__', () => { assert.throws(() => set({}, 'a.__proto__.b', 'c')) assert.throws(() => set({}, 'a.__proto__', 'c')) assert.throws(() => set({}, '__proto__', 'c')) }) }) describe('set', () => { it('should support immediate properties', () => { const o = {} set(o, 'a', 'b') assert.strictEqual(o.a, 'b') }) it('should return non-objects', () => { const str = set('foo', 'a.b', 'c') assert.strictEqual(str, 'foo') const _null = set(null, 'a.b', 'c') assert.strictEqual(_null, null) }) it('should set when key is a symbol', () => { const key = Symbol('foo') const obj = {} set(obj, key, 'bar') assert.strictEqual(obj[key], 'bar') }) it('should set on the root of the object', () => { const o = {} set(o, 'foo', 'bar') assert.strictEqual(o.foo, 'bar') }) it('deep property can be set', function () { set(obj, 'one.two.three', 'test2') assert.strictEqual(obj.one.two.three, 'test2') }) it('should set the specified property.', () => { assert.deepStrictEqual(set({ a: 'aaa', b: 'b' }, 'a', 'bbb'), { a: 'bbb', b: 'b' }) }) it('should set a nested property', () => { const o = {} set(o, 'a.b', 'c') assert.strictEqual(o.a.b, 'c') }) it('should set a nested property where the last key is a symbol', () => { const o = {} set(o, 'a.b', 'c') assert.strictEqual(o.a.b, 'c') }) it('should support passing an array as the key', () => { const actual = set({ a: 'a', b: { c: 'd' } }, ['b', 'c', 'd'], 'eee') assert.deepStrictEqual(actual, { a: 'a', b: { c: { d: 'eee' } } }) }) it('should set a deeply nested value.', () => { const actual = set({ a: 'a', b: { c: 'd' } }, 'b.c.d', 'eee') assert.deepStrictEqual(actual, { a: 'a', b: { c: { d: 'eee' } } }) }) it('should allow keys to be whitespace', () => { const o = {} set(o, 'a. .a', { y: 'z' }) assert.deepStrictEqual(o.a[' '].a, { y: 'z' }) }) it('should set an array element', () => { const o = { a: ['zero', 'one'] } set(o, 'a.0', { y: 'z' }) assert(Array.isArray(o.a)) assert.deepStrictEqual(o.a[0], { y: 'z' }) assert.deepStrictEqual(o.a[1], 'one') }) it('should create an array if it does not already exist', () => { const o = {} set(o, 'a.0', 'zero') set(o, 'a.1.b', { y: 'z' }) set(o, 'a.1.b.x', 'eggs') set(o, 'c.1', 'see') set(o, '0', 'not array') assert(check.isArray(o.a)) assert.deepStrictEqual(o.a[0], 'zero') assert.deepStrictEqual(o.a[1].b, { y: 'z', x: 'eggs' }) assert.deepStrictEqual(o.c[1], 'see') assert.deepStrictEqual(o[0], 'not array') }) it('should extend a function', () => { const log = () => { } const warning = () => { } const o = {} set(o, 'helpers.foo', log) set(o, 'helpers.foo.warning', warning) assert(check.isFunction(o.helpers.foo)) assert(check.isFunction(o.helpers.foo)) }) it('should extend an object in an array', () => { const o = { a: [{}, {}, {}] } set(o, 'a.0.a', { y: 'z' }) set(o, 'a.1.b', { y: 'z' }) set(o, 'a.2.c', { y: 'z' }) assert(Array.isArray(o.a)) assert.deepStrictEqual(o.a[0].a, { y: 'z' }) assert.deepStrictEqual(o.a[1].b, { y: 'z' }) assert.deepStrictEqual(o.a[2].c, { y: 'z' }) }) it('should not create a nested property if it does already exist', () => { const first = { name: 'Halle' } const o = { a: first } set(o, 'a.b', 'c') assert.strictEqual(o.a.b, 'c') assert.strictEqual(o.a, first) assert.strictEqual(o.a.name, 'Halle') }) it('should use property paths to set nested values from the source object.', () => { const o = {} set(o, 'a.locals.name', { first: 'Brian' }) set(o, 'b.locals.name', { last: 'Woodward' }) set(o, 'b.locals.name.last', 'Woodward') assert.deepStrictEqual(o, { a: { locals: { name: { first: 'Brian' } } }, b: { locals: { name: { last: 'Woodward' } } } }) }) it('should delete the property when value is undefined', () => { const o = {} assert.deepStrictEqual(set(o, 'a.locals.name', 'test'), o) assert.deepStrictEqual(set(o, 'b.locals.name'), o) assert.deepStrictEqual(set(o, 'a.locals.name'), { a: { locals: {} } }) assert.deepStrictEqual(set({ a: 'a', b: { c: 'd' } }, 'b.c'), { a: 'a', b: {} }) }) it('should return the entire object if no or improper path is passed.', () => { assert.deepStrictEqual(set({ a: 'a', b: { c: 'd' } }), { a: 'a', b: { c: 'd' } }) }) it('should set non-plain objects', done => { const o = {} set(o, 'a.b', new Date()) const firstDate = o.a.b.getTime() setTimeout(function () { set(o, 'a.b', new Date()) const secondDate = o.a.b.getTime() assert.notDeepStrictEqual(firstDate, secondDate) done() }, 10) }) }) describe('escaping', () => { it('should not split escaped dots. Works with set, get or del', () => { const o = {} set(o, 'a\\.b.c.d.e', 'test') assert.strictEqual(o['a.b'].c.d.e, 'test') assert.strictEqual(get(o, 'a\\.b.c.d.e'), 'test') assert.deepStrictEqual(del(o, 'a\\.b.c.d.e'), { 'a.b': { c: { d: {} } } }) }) it('should work with multiple escaped dots', () => { const obj1 = {} set(obj1, 'e\\.f\\.g', 1) assert.strictEqual(obj1['e.f.g'], 1) const obj2 = {} set(obj2, 'e\\.f.g\\.h\\.i.j', 1) assert.deepStrictEqual(obj2, { 'e.f': { 'g.h.i': { j: 1 } } }) }) it('should work with multiple escaped any separator', () => { const obj2 = {} set(obj2, 'e\\.f.g\\/h\\%i.j', 1, { separator: './%' }) assert.deepStrictEqual(obj2, { 'e.f': { 'g/h%i': { j: 1 } } }) }) }) describe('options.merge', () => { it('should merge an existing value with the given value', () => { const o = { a: { b: { c: 'd' } } } set(o, 'a.b', { y: 'z' }, { merge: true }) assert.deepStrictEqual(o.a.b, { c: 'd', y: 'z' }) const obj = { foo: { bar: { baz: 'qux' } } } set(obj, 'foo.bar.fez', 'zzz', { merge: true }) assert.deepStrictEqual(obj, { foo: { bar: { baz: 'qux', fez: 'zzz' } } }) }) it('should update an object by merging values', () => { const o = {} set(o, 'a', { b: 'c' }) set(o, 'a', { c: 'd' }, { merge: true }) assert.deepStrictEqual(o, { a: { b: 'c', c: 'd' } }) set(o, 'a', 'b') assert.strictEqual(o.a, 'b') }) }) describe('options.separator', () => { it('should allow a custom separator', () => { const o = {} set(o, 'a/b/c/d/e', 'c', { separator: '/' }) assert.strictEqual(o.a.b.c.d.e, 'c') }) it('should allow multiple custom separators', () => { const o = {} set(o, 'a/b;c/d.e', 'c', { separator: '/;.' }) assert.strictEqual(o.a.b.c.d.e, 'c') }) it('should accept "delimiter" in place of "separator"', () => { const o = {} set(o, 'a/b;c/d.e', 'c', { delimiter: '/;.' }) assert.strictEqual(o.a.b.c.d.e, 'c') }) it('should split any string elements of an array path', () => { const o = {} const key = Symbol('key-1') set(o, ['z.b', key, 'c'], 'd') assert.strictEqual(o.z.b[key].c, 'd') }) it('should not split any string elements of an array path if requested', () => { const o = {} const key = Symbol('key-1') set(o, ['a.b', key, 'c'], 'd', { elSplit: false }) assert.strictEqual(o['a.b'][key].c, 'd') }) }) describe('options.split', () => { it('should use a custom function to split on , not .', () => { const o = {} const splitfn = (path) => { return path.split(',') } set(o, 'a,b.c.d,e', 'c', { split: splitfn }) assert.strictEqual(o.a['b.c.d'].e, 'c') }) }) describe('helpers', () => { it('getKeys: will split path as string or array into array of single keys', () => { const test = 'atest' const key = Symbol('akey') assert.deepStrictEqual(getKeys('this.is\\.a.string'), ['this', 'is.a', 'string']) assert.deepStrictEqual(getKeys(['this.is.a.string', key, test]), ['this', 'is', 'a', 'string', key, 'atest']) }) it('pathToString: converts path with getKeys and returns joined string', () => { const p1 = ['this', 'is%an', 'array'] const p2 = [['this', 'is%an', 'array'], { dot: '#', delimiter: '.%' }] // console.log(pathToString(p1)) // console.log(pathToString(...p2)) assert.deepStrictEqual(pathToString(p1), 'this.is%an.array') assert.deepStrictEqual(pathToString(...p2), 'this#is#an#array') }) it('pathToString: returns null if unable to make string (e.g. path contains Symbol)', () => { const p1 = [['this', Symbol('test'), 'array'], { dot: '#', delimiter: '.%' }] // console.log('has symbol', pathToString(...p1)) assert.strictEqual(pathToString(...p1), null) }) it('walkPath: will walk an array of keys deep into object', () => { const path = 'this.is.a.value' const path2 = 'this.is.a.bogus.value' const path3 = 'this.is.object' const o = {} set(o, path, 'some value') set(o, path3, { a: 'what', b: 'when' }) assert.strictEqual(walkPath(o, getKeys(path)), 'some value') assert.strictEqual(walkPath(o, getKeys(path2)), undefined) assert.deepStrictEqual(walkPath(o, getKeys(path3)), { a: 'what', b: 'when' }) }) })