From db6542a554fbee04fb041bf504b4f1787af6a1db Mon Sep 17 00:00:00 2001 From: Adam Stankiewicz Date: Tue, 15 Sep 2015 15:23:26 +0200 Subject: [PATCH] Guarantee isomorphism of flatten and unflatten commands If no changes are needed when flattening / unflattening, the same object is returned, saving memory and allowing for strict equality checks on results of flatten/unflatten. This commit also adds "shallow" option for unflatten command that ignores nested "messy" object. It is especially useful when used with "maxDepth" option of flatten function. For example, now following is true: ``` var object1 = { "foo": { "bar": { "baz": "buz" } } } var object2 = flatten(object1, { maxDepth: 2 }) var object3 = unflatten(object2, { shallow: true }) assert(object1.foo.bar === object2['foo.bar']) assert(object1.foo.bar === object3.foo.bar) ``` --- README.md | 12 ++++++++ index.js | 33 +++++++++++++++++++--- test/test.js | 79 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 120 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 2e4cb3c..5368914 100644 --- a/README.md +++ b/README.md @@ -71,6 +71,7 @@ Use a custom delimiter for (un)flattening your objects, instead of `.`. When enabled, both `flat` and `unflatten` will preserve arrays and their contents. This is disabled by default. + ``` javascript var flatten = require('flat') @@ -158,3 +159,14 @@ flatten({ // 'key3.a': { b: { c: 2 } } // } ``` +### shallow + +When enabled, nested flattened objects are preserved when unflattening. + +``` +unflatten({ "foo.bar": { "fiz.fuz": "hello" }}) +// { foo: { bar: { "fiz": { "fuz": "hello" } } } + +unflatten({ "foo.bar": { "fiz.fuz": "hello" }}, { shallow: true }) +// { foo: { bar: { "fiz.fuz": "hello" } } +``` diff --git a/index.js b/index.js index 4d6914a..932cb4b 100644 --- a/index.js +++ b/index.js @@ -10,6 +10,8 @@ function flatten(target, opts) { var currentDepth = 1 var output = {} + var changed = false + function step(object, prev) { Object.keys(object).forEach(function(key) { var value = object[key] @@ -34,19 +36,28 @@ function flatten(target, opts) { return step(value, newKey) } + if (newKey !== key) { + changed = true + } + output[newKey] = value }) } step(target) - return output + if (changed) { + return output + } else { + return target + } } function unflatten(target, opts) { opts = opts || {} var delimiter = opts.delimiter || '.' + var shallow = opts.shallow || false var overwrite = opts.overwrite || false var result = {} @@ -55,6 +66,8 @@ function unflatten(target, opts) { return target } + var changed = false + // safely ensure that the key is // an integer. function getkey(key) { @@ -74,6 +87,8 @@ function unflatten(target, opts) { var recipient = result while (key2 !== undefined) { + changed = true + var type = Object.prototype.toString.call(recipient[key1]) var isobject = ( type === "[object Object]" || @@ -94,11 +109,21 @@ function unflatten(target, opts) { } } - // unflatten again for 'messy objects' - recipient[key1] = unflatten(target[key], opts) + if (!shallow) { + // unflatten again for 'messy objects' + var value = target[key] + recipient[key1] = unflatten(target[key], opts) + changed = changed || value !== recipient[key1] + } else { + recipient[key1] = target[key] + } }) - return result + if (changed) { + return result + } else { + return target + } } function isBuffer(value) { diff --git a/test/test.js b/test/test.js index 707901b..65e7fd9 100644 --- a/test/test.js +++ b/test/test.js @@ -12,6 +12,20 @@ var primitives = { , undefined: undefined } +test('Isomorphism', function() { + var original = { foo: 'bar' } + + assert.strictEqual( + original, + flatten(unflatten(original)) + ) + + assert.strictEqual( + original, + unflatten(flatten(original)) + ) +}); + suite('Flatten Primitives', function() { Object.keys(primitives).forEach(function(key) { var value = primitives[key] @@ -115,6 +129,12 @@ suite('Flatten', function() { }) }) + test('Identity', function() { + var object = { foo: 'baz', fiz: 'fuz' } + + assert.strictEqual(flatten(object), flatten(object)) + }) + if (typeof Buffer !== 'undefined') test('Buffer', function() { assert.deepEqual(flatten({ hello: { @@ -263,6 +283,65 @@ suite('Unflatten', function() { })) }) + test('Identity', function() { + var object = { foo: { bar: 'baz' } } + + assert.strictEqual(unflatten(object), unflatten(object)) + }); + + suite('.shallow', function() { + test('Should leave nested objects untouched', function() { + assert.deepEqual(unflatten({ + 'hello.world': { 'foo.fiz': 'bar' } + }, { shallow: true }), { + 'hello': { + 'world': { + 'foo.fiz': 'bar' + } + } + }) + }); + + test('Should preserve object identity', function() { + var object = { + 'hello.world': { foo: 'bar' } + } + + var unflattened1 = unflatten(object, { shallow: true }) + var unflattened2 = unflatten(object, { shallow: true }) + + assert.deepEqual(unflattened1.hello.world, { foo: 'bar' }) + assert.strictEqual(unflattened1.hello.world, unflattened2.hello.world) + }) + + test('Identity', function() { + var object = { foo: { "ir.re.le.vant": 'baz' } } + + assert.strictEqual( + unflatten(object, { shallow: true }), + unflatten(object, { shallow: true }) + ) + }); + + test('Isomorphism 1', function() { + var object = { foo: { "ir.re.le.vant": 'baz' } } + + assert.strictEqual( + object, + flatten(unflatten(object, { shallow: true }), { maxDepth: 1 }) + ) + }); + + test('Isomorphism 2', function() { + var object = { foo: { "ir.re.le.vant": 'baz' } } + + assert.strictEqual( + object, + unflatten(flatten(object, { maxDepth: 1 }), { shallow: true }) + ) + }); + }) + suite('.safe', function() { test('Should protect arrays when true', function() { assert.deepEqual(flatten({