uci-utils-obj-nested-prop/test/obj-nested-prop.test.js

333 lines
11 KiB
JavaScript

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(['one', ['two', 'three', 'four']]), ['one', 'two', 'three', 'four'])
assert(!getKeys(['one', ['two', ['three']]]))
assert.deepStrictEqual(getKeys('this.is\\.a.string'), ['this', 'is.a', 'string'])
assert.deepStrictEqual(getKeys(['this.is.a.string', ['some\\.nested.keys', key], key, test]),
['this', 'is', 'a', 'string', 'some.nested', 'keys', key, 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' })
})
})