/* globals suite test */
const assert = require('assert')
const path = require('path')
const { exec } = require('child_process')
const pkg = require('../package.json')
const flat = require('../index')
const flatten = flat.flatten
const unflatten = flat.unflatten
const primitives = {
String: 'good morning',
Number: 1234.99,
Boolean: true,
Date: new Date(),
null: null,
undefined: undefined
}
suite('Flatten Primitives', function () {
Object.keys(primitives).forEach(function (key) {
const value = primitives[key]
test(key, function () {
assert.deepStrictEqual(flatten({
hello: {
world: value
}
}), {
'hello.world': value
})
})
})
})
suite('Unflatten Primitives', function () {
Object.keys(primitives).forEach(function (key) {
const value = primitives[key]
test(key, function () {
assert.deepStrictEqual(unflatten({
'hello.world': value
}), {
hello: {
world: value
}
})
})
})
})
suite('Flatten', function () {
test('Nested once', function () {
assert.deepStrictEqual(flatten({
hello: {
world: 'good morning'
}
}), {
'hello.world': 'good morning'
})
})
test('Nested twice', function () {
assert.deepStrictEqual(flatten({
hello: {
world: {
again: 'good morning'
}
}
}), {
'hello.world.again': 'good morning'
})
})
test('Multiple Keys', function () {
assert.deepStrictEqual(flatten({
hello: {
lorem: {
ipsum: 'again',
dolor: 'sit'
}
},
world: {
lorem: {
ipsum: 'again',
dolor: 'sit'
}
}
}), {
'hello.lorem.ipsum': 'again',
'hello.lorem.dolor': 'sit',
'world.lorem.ipsum': 'again',
'world.lorem.dolor': 'sit'
})
})
test('Custom Delimiter', function () {
assert.deepStrictEqual(flatten({
hello: {
world: {
again: 'good morning'
}
}
}, {
delimiter: ':'
}), {
'hello:world:again': 'good morning'
})
})
test('Empty Objects', function () {
assert.deepStrictEqual(flatten({
hello: {
empty: {
nested: {}
}
}
}), {
'hello.empty.nested': {}
})
})
if (typeof Buffer !== 'undefined') {
test('Buffer', function () {
assert.deepStrictEqual(flatten({
hello: {
empty: {
nested: Buffer.from('test')
}
}
}), {
'hello.empty.nested': Buffer.from('test')
})
})
}
if (typeof Uint8Array !== 'undefined') {
test('typed arrays', function () {
assert.deepStrictEqual(flatten({
hello: {
empty: {
nested: new Uint8Array([1, 2, 3, 4])
}
}
}), {
'hello.empty.nested': new Uint8Array([1, 2, 3, 4])
})
})
}
test('Custom Depth', function () {
assert.deepStrictEqual(flatten({
hello: {
world: {
again: 'good morning'
}
},
lorem: {
ipsum: {
dolor: 'good evening'
}
}
}, {
maxDepth: 2
}), {
'hello.world': {
again: 'good morning'
},
'lorem.ipsum': {
dolor: 'good evening'
}
})
})
test('Transformed Keys', function () {
assert.deepStrictEqual(flatten({
hello: {
world: {
again: 'good morning'
}
},
lorem: {
ipsum: {
dolor: 'good evening'
}
}
}, {
transformKey: function (key) {
return '__' + key + '__'
}
}), {
'__hello__.__world__.__again__': 'good morning',
'__lorem__.__ipsum__.__dolor__': 'good evening'
})
})
test('Should keep number in the left when object', function () {
assert.deepStrictEqual(flatten({
hello: {
'0200': 'world',
'0500': 'darkness my old friend'
}
}), {
'hello.0200': 'world',
'hello.0500': 'darkness my old friend'
})
})
})
suite('Unflatten', function () {
test('Nested once', function () {
assert.deepStrictEqual({
hello: {
world: 'good morning'
}
}, unflatten({
'hello.world': 'good morning'
}))
})
test('Nested twice', function () {
assert.deepStrictEqual({
hello: {
world: {
again: 'good morning'
}
}
}, unflatten({
'hello.world.again': 'good morning'
}))
})
test('Multiple Keys', function () {
assert.deepStrictEqual({
hello: {
lorem: {
ipsum: 'again',
dolor: 'sit'
}
},
world: {
greet: 'hello',
lorem: {
ipsum: 'again',
dolor: 'sit'
}
}
}, unflatten({
'hello.lorem.ipsum': 'again',
'hello.lorem.dolor': 'sit',
'world.lorem.ipsum': 'again',
'world.lorem.dolor': 'sit',
world: { greet: 'hello' }
}))
})
test('nested objects do not clobber each other when a.b inserted before a', function () {
const x = {}
x['foo.bar'] = { t: 123 }
x.foo = { p: 333 }
assert.deepStrictEqual(unflatten(x), {
foo: {
bar: {
t: 123
},
p: 333
}
})
})
test('Custom Delimiter', function () {
assert.deepStrictEqual({
hello: {
world: {
again: 'good morning'
}
}
}, unflatten({
'hello world again': 'good morning'
}, {
delimiter: ' '
}))
})
test('Overwrite', function () {
assert.deepStrictEqual({
travis: {
build: {
dir: '/home/travis/build/kvz/environmental'
}
}
}, unflatten({
travis: 'true',
travis_build_dir: '/home/travis/build/kvz/environmental'
}, {
delimiter: '_',
overwrite: true
}))
})
test('Transformed Keys', function () {
assert.deepStrictEqual(unflatten({
'__hello__.__world__.__again__': 'good morning',
'__lorem__.__ipsum__.__dolor__': 'good evening'
}, {
transformKey: function (key) {
return key.substring(2, key.length - 2)
}
}), {
hello: {
world: {
again: 'good morning'
}
},
lorem: {
ipsum: {
dolor: 'good evening'
}
}
})
})
test('Messy', function () {
assert.deepStrictEqual({
hello: { world: 'again' },
lorem: { ipsum: 'another' },
good: {
morning: {
hash: {
key: {
nested: {
deep: {
and: {
even: {
deeper: { still: 'hello' }
}
}
}
}
}
},
again: { testing: { this: 'out' } }
}
}
}, unflatten({
'hello.world': 'again',
'lorem.ipsum': 'another',
'good.morning': {
'hash.key': {
'nested.deep': {
'and.even.deeper.still': 'hello'
}
}
},
'good.morning.again': {
'testing.this': 'out'
}
}))
})
suite('Overwrite + non-object values in key positions', function () {
test('non-object keys + overwrite should be overwritten', function () {
assert.deepStrictEqual(flat.unflatten({ a: null, 'a.b': 'c' }, { overwrite: true }), { a: { b: 'c' } })
assert.deepStrictEqual(flat.unflatten({ a: 0, 'a.b': 'c' }, { overwrite: true }), { a: { b: 'c' } })
assert.deepStrictEqual(flat.unflatten({ a: 1, 'a.b': 'c' }, { overwrite: true }), { a: { b: 'c' } })
assert.deepStrictEqual(flat.unflatten({ a: '', 'a.b': 'c' }, { overwrite: true }), { a: { b: 'c' } })
})
test('overwrite value should not affect undefined keys', function () {
assert.deepStrictEqual(flat.unflatten({ a: undefined, 'a.b': 'c' }, { overwrite: true }), { a: { b: 'c' } })
assert.deepStrictEqual(flat.unflatten({ a: undefined, 'a.b': 'c' }, { overwrite: false }), { a: { b: 'c' } })
})
test('if no overwrite, should ignore nested values under non-object key', function () {
assert.deepStrictEqual(flat.unflatten({ a: null, 'a.b': 'c' }), { a: null })
assert.deepStrictEqual(flat.unflatten({ a: 0, 'a.b': 'c' }), { a: 0 })
assert.deepStrictEqual(flat.unflatten({ a: 1, 'a.b': 'c' }), { a: 1 })
assert.deepStrictEqual(flat.unflatten({ a: '', 'a.b': 'c' }), { a: '' })
})
})
suite('.safe', function () {
test('Should protect arrays when true', function () {
assert.deepStrictEqual(flatten({
hello: [
{ world: { again: 'foo' } },
{ lorem: 'ipsum' }
],
another: {
nested: [{ array: { too: 'deep' } }]
},
lorem: {
ipsum: 'whoop'
}
}, {
safe: true
}), {
hello: [
{ world: { again: 'foo' } },
{ lorem: 'ipsum' }
],
'lorem.ipsum': 'whoop',
'another.nested': [{ array: { too: 'deep' } }]
})
})
test('Should not protect arrays when false', function () {
assert.deepStrictEqual(flatten({
hello: [
{ world: { again: 'foo' } },
{ lorem: 'ipsum' }
]
}, {
safe: false
}), {
'hello.0.world.again': 'foo',
'hello.1.lorem': 'ipsum'
})
})
test('Empty objects should not be removed', function () {
assert.deepStrictEqual(unflatten({
foo: [],
bar: {}
}), { foo: [], bar: {} })
})
})
suite('.object', function () {
test('Should create object instead of array when true', function () {
const unflattened = unflatten({
'hello.you.0': 'ipsum',
'hello.you.1': 'lorem',
'hello.other.world': 'foo'
}, {
object: true
})
assert.deepStrictEqual({
hello: {
you: {
0: 'ipsum',
1: 'lorem'
},
other: { world: 'foo' }
}
}, unflattened)
assert(!Array.isArray(unflattened.hello.you))
})
test('Should create object instead of array when nested', function () {
const unflattened = unflatten({
hello: {
'you.0': 'ipsum',
'you.1': 'lorem',
'other.world': 'foo'
}
}, {
object: true
})
assert.deepStrictEqual({
hello: {
you: {
0: 'ipsum',
1: 'lorem'
},
other: { world: 'foo' }
}
}, unflattened)
assert(!Array.isArray(unflattened.hello.you))
})
test('Should keep the zero in the left when object is true', function () {
const unflattened = unflatten({
'hello.0200': 'world',
'hello.0500': 'darkness my old friend'
}, {
object: true
})
assert.deepStrictEqual({
hello: {
'0200': 'world',
'0500': 'darkness my old friend'
}
}, unflattened)
})
test('Should not create object when false', function () {
const unflattened = unflatten({
'hello.you.0': 'ipsum',
'hello.you.1': 'lorem',
'hello.other.world': 'foo'
}, {
object: false
})
assert.deepStrictEqual({
hello: {
you: ['ipsum', 'lorem'],
other: { world: 'foo' }
}
}, unflattened)
assert(Array.isArray(unflattened.hello.you))
})
})
if (typeof Buffer !== 'undefined') {
test('Buffer', function () {
assert.deepStrictEqual(unflatten({
'hello.empty.nested': Buffer.from('test')
}), {
hello: {
empty: {
nested: Buffer.from('test')
}
}
})
})
}
if (typeof Uint8Array !== 'undefined') {
test('typed arrays', function () {
assert.deepStrictEqual(unflatten({
'hello.empty.nested': new Uint8Array([1, 2, 3, 4])
}), {
hello: {
empty: {
nested: new Uint8Array([1, 2, 3, 4])
}
}
})
})
}
test('should not pollute prototype', function () {
unflatten({
'__proto__.polluted': true
})
unflatten({
'prefix.__proto__.polluted': true
})
unflatten({
'prefix.0.__proto__.polluted': true
})
assert.notStrictEqual({}.polluted, true)
})
})
suite('Arrays', function () {
test('Should be able to flatten arrays properly', function () {
assert.deepStrictEqual({
'a.0': 'foo',
'a.1': 'bar'
}, flatten({
a: ['foo', 'bar']
}))
})
test('Should be able to revert and reverse array serialization via unflatten', function () {
assert.deepStrictEqual({
a: ['foo', 'bar']
}, unflatten({
'a.0': 'foo',
'a.1': 'bar'
}))
})
test('Array typed objects should be restored by unflatten', function () {
assert.strictEqual(
Object.prototype.toString.call(['foo', 'bar'])
, Object.prototype.toString.call(unflatten({
'a.0': 'foo',
'a.1': 'bar'
}).a)
)
})
test('Do not include keys with numbers inside them', function () {
assert.deepStrictEqual(unflatten({
'1key.2_key': 'ok'
}), {
'1key': {
'2_key': 'ok'
}
})
})
})
suite('Order of Keys', function () {
test('Order of keys should not be changed after round trip flatten/unflatten', function () {
const obj = {
b: 1,
abc: {
c: [{
d: 1,
bca: 1,
a: 1
}]
},
a: 1
}
const result = unflatten(
flatten(obj)
)
assert.deepStrictEqual(Object.keys(obj), Object.keys(result))
assert.deepStrictEqual(Object.keys(obj.abc), Object.keys(result.abc))
assert.deepStrictEqual(Object.keys(obj.abc.c[0]), Object.keys(result.abc.c[0]))
})
})
suite('CLI', function () {
test('can take filename', function (done) {
const cli = path.resolve(__dirname, '..', pkg.bin)
const pkgJSON = path.resolve(__dirname, '..', 'package.json')
exec(`${cli} ${pkgJSON}`, (err, stdout, stderr) => {
assert.ifError(err)
assert.strictEqual(stdout.trim(), JSON.stringify(flatten(pkg), null, 2))
done()
})
})
test('exits with usage if no file', function (done) {
const cli = path.resolve(__dirname, '..', pkg.bin)
const pkgJSON = path.resolve(__dirname, '..', 'package.json')
exec(`${cli} ${pkgJSON}`, (err, stdout, stderr) => {
assert.ifError(err)
assert.strictEqual(stdout.trim(), JSON.stringify(flatten(pkg), null, 2))
done()
})
})
test('can take piped file', function (done) {
const cli = path.resolve(__dirname, '..', pkg.bin)
const pkgJSON = path.resolve(__dirname, '..', 'package.json')
exec(`cat ${pkgJSON} | ${cli}`, (err, stdout, stderr) => {
assert.ifError(err)
assert.strictEqual(stdout.trim(), JSON.stringify(flatten(pkg), null, 2))
done()
})
})
})