diff --git a/.gitignore b/.gitignore index 81873ba..52485e1 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ node_modules/ npm-debug.log yarn.lock +package-lock.json diff --git a/index.d.ts b/index.d.ts index 56f6865..9a9b1f0 100644 --- a/index.d.ts +++ b/index.d.ts @@ -1,8 +1,23 @@ -declare function stringify(value: any, replacer?: (key: string, value: any) => any, space?: string | number): string; +declare function stringify( + value: any, + replacer?: (key: string, value: any) => any, + space?: string | number, + options?: { depthLimit: number | undefined; edgesLimit: number | undefined } +): string; declare namespace stringify { - export function stable(value: any, replacer?: (key: string, value: any) => any, space?: string | number): string; - export function stableStringify(value: any, replacer?: (key: string, value: any) => any, space?: string | number): string; + export function stable( + value: any, + replacer?: (key: string, value: any) => any, + space?: string | number, + options?: { depthLimit: number | undefined; edgesLimit: number | undefined } + ): string; + export function stableStringify( + value: any, + replacer?: (key: string, value: any) => any, + space?: string | number, + options?: { depthLimit: number | undefined; edgesLimit: number | undefined } + ): string; } export default stringify; diff --git a/index.js b/index.js index e29ffa5..921314f 100644 --- a/index.js +++ b/index.js @@ -3,12 +3,26 @@ stringify.default = stringify stringify.stable = deterministicStringify stringify.stableStringify = deterministicStringify +var LIMIT_REPLACE_NODE = '[...]' +var CIRCULAR_REPLACE_NODE = '[Circular]' + var arr = [] var replacerStack = [] +function defaultOptions () { + return { + depthLimit: 10, + edgesLimit: 20 + } +} + // Regular stringify -function stringify (obj, replacer, spacer) { - decirc(obj, '', [], undefined) +function stringify (obj, replacer, spacer, options) { + if (typeof options === 'undefined') { + options = defaultOptions() + } + + decirc(obj, '', 0, [], undefined, 0, options) var res try { if (replacerStack.length === 0) { @@ -30,37 +44,60 @@ function stringify (obj, replacer, spacer) { } return res } -function decirc (val, k, stack, parent) { + +function setReplace (replace, val, k, parent) { + var propertyDescriptor = Object.getOwnPropertyDescriptor(parent, k) + if (propertyDescriptor.get !== undefined) { + if (propertyDescriptor.configurable) { + Object.defineProperty(parent, k, { value: replace }) + arr.push([parent, k, val, propertyDescriptor]) + } else { + replacerStack.push([val, k, replace]) + } + } else { + parent[k] = replace + arr.push([parent, k, val]) + } +} + +function decirc (val, k, edgeIndex, stack, parent, depth, options) { + depth += 1 var i if (typeof val === 'object' && val !== null) { for (i = 0; i < stack.length; i++) { if (stack[i] === val) { - var propertyDescriptor = Object.getOwnPropertyDescriptor(parent, k) - if (propertyDescriptor.get !== undefined) { - if (propertyDescriptor.configurable) { - Object.defineProperty(parent, k, { value: '[Circular]' }) - arr.push([parent, k, val, propertyDescriptor]) - } else { - replacerStack.push([val, k]) - } - } else { - parent[k] = '[Circular]' - arr.push([parent, k, val]) - } + setReplace(CIRCULAR_REPLACE_NODE, val, k, parent) return } } + + if ( + typeof options.depthLimit !== 'undefined' && + depth > options.depthLimit + ) { + setReplace(LIMIT_REPLACE_NODE, val, k, parent) + return + } + + if ( + typeof options.edgesLimit !== 'undefined' && + edgeIndex + 1 > options.edgesLimit + ) { + setReplace(LIMIT_REPLACE_NODE, val, k, parent) + return + } + stack.push(val) // Optimize for Arrays. Big arrays could kill the performance otherwise! if (Array.isArray(val)) { for (i = 0; i < val.length; i++) { - decirc(val[i], i, stack, val) + decirc(val[i], i, i, stack, val, depth, options) } } else { var keys = Object.keys(val) for (i = 0; i < keys.length; i++) { var key = keys[i] - decirc(val[key], key, stack, val) + decirc(val[key], key, i, stack, val, depth, options) } } stack.pop() @@ -78,8 +115,12 @@ function compareFunction (a, b) { return 0 } -function deterministicStringify (obj, replacer, spacer) { - var tmp = deterministicDecirc(obj, '', [], undefined) || obj +function deterministicStringify (obj, replacer, spacer, options) { + if (typeof options === 'undefined') { + options = defaultOptions() + } + + var tmp = deterministicDecirc(obj, '', 0, [], undefined, 0, options) || obj var res try { if (replacerStack.length === 0) { @@ -103,23 +144,13 @@ function deterministicStringify (obj, replacer, spacer) { return res } -function deterministicDecirc (val, k, stack, parent) { +function deterministicDecirc (val, k, edgeIndex, stack, parent, depth, options) { + depth += 1 var i if (typeof val === 'object' && val !== null) { for (i = 0; i < stack.length; i++) { if (stack[i] === val) { - var propertyDescriptor = Object.getOwnPropertyDescriptor(parent, k) - if (propertyDescriptor.get !== undefined) { - if (propertyDescriptor.configurable) { - Object.defineProperty(parent, k, { value: '[Circular]' }) - arr.push([parent, k, val, propertyDescriptor]) - } else { - replacerStack.push([val, k]) - } - } else { - parent[k] = '[Circular]' - arr.push([parent, k, val]) - } + setReplace(CIRCULAR_REPLACE_NODE, val, k, parent) return } } @@ -130,11 +161,28 @@ function deterministicDecirc (val, k, stack, parent) { } catch (_) { return } + + if ( + typeof options.depthLimit !== 'undefined' && + depth > options.depthLimit + ) { + setReplace(LIMIT_REPLACE_NODE, val, k, parent) + return + } + + if ( + typeof options.edgesLimit !== 'undefined' && + edgeIndex + 1 > options.edgesLimit + ) { + setReplace(LIMIT_REPLACE_NODE, val, k, parent) + return + } + stack.push(val) // Optimize for Arrays. Big arrays could kill the performance otherwise! if (Array.isArray(val)) { for (i = 0; i < val.length; i++) { - deterministicDecirc(val[i], i, stack, val) + deterministicDecirc(val[i], i, i, stack, val, depth, options) } } else { // Create a temporary object in the required way @@ -142,10 +190,10 @@ function deterministicDecirc (val, k, stack, parent) { var keys = Object.keys(val).sort(compareFunction) for (i = 0; i < keys.length; i++) { var key = keys[i] - deterministicDecirc(val[key], key, stack, val) + deterministicDecirc(val[key], key, i, stack, val, depth, options) tmp[key] = val[key] } - if (parent !== undefined) { + if (typeof parent !== 'undefined') { arr.push([parent, k, val]) parent[k] = tmp } else { @@ -157,15 +205,20 @@ function deterministicDecirc (val, k, stack, parent) { } // wraps replacer function to handle values we couldn't replace -// and mark them as [Circular] +// and mark them as replaced value function replaceGetterValues (replacer) { - replacer = replacer !== undefined ? replacer : function (k, v) { return v } + replacer = + typeof replacer !== 'undefined' + ? replacer + : function (k, v) { + return v + } return function (key, val) { if (replacerStack.length > 0) { for (var i = 0; i < replacerStack.length; i++) { var part = replacerStack[i] if (part[1] === key && part[0] === val) { - val = '[Circular]' + val = part[2] replacerStack.splice(i, 1) break } diff --git a/readme.md b/readme.md index cb4446d..fa6efa1 100644 --- a/readme.md +++ b/readme.md @@ -13,7 +13,7 @@ handle circular structures. See the example below for further information. The same as [JSON.stringify][]. -`stringify(value[, replacer[, space]])` +`stringify(value[, replacer[, space[, options]]])` ```js const safeStringify = require('fast-safe-stringify') @@ -33,7 +33,14 @@ function replacer(key, value) { } return value } -const serialized = safeStringify(o, replacer, 2) + +// those are also defaults limits when no options object is passed into safeStringify +const options = { + depthLimit: 10, + edgesLimit: 20, +}; + +const serialized = safeStringify(o, replacer, 2, options) // Key: "" Value: {"a":1,"o":"[Circular]"} // Key: "a" Value: 1 // Key: "o" Value: "[Circular]" @@ -43,6 +50,7 @@ console.log(serialized) // } ``` + Using the deterministic version also works the same: ```js @@ -62,6 +70,11 @@ A faster and side-effect free implementation is available in the [safe-stable-stringify][] module. However it is still considered experimental due to a new and more complex implementation. +### Replace strings constants + +- `[Circular]` - when same reference is found +- `[...]` - when some limit from options object is reached + ## Differences to JSON.stringify In general the behavior is identical to [JSON.stringify][]. The [`replacer`][] @@ -153,4 +166,4 @@ MIT [`space`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#The%20space%20argument [`toJSON`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#toJSON()_behavior [benchmark]: https://github.com/epoberezkin/fast-json-stable-stringify/blob/67f688f7441010cfef91a6147280cc501701e83b/benchmark -[JSON.stringify]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify +[JSON.stringify]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify \ No newline at end of file diff --git a/test-stable.js b/test-stable.js index 3d26f77..c55b95c 100644 --- a/test-stable.js +++ b/test-stable.js @@ -7,11 +7,9 @@ const stream = require('stream') test('circular reference to root', function (assert) { const fixture = { name: 'Tywin Lannister' } fixture.circle = fixture - const expected = s( - { circle: '[Circular]', name: 'Tywin Lannister' } - ) + const expected = s({ circle: '[Circular]', name: 'Tywin Lannister' }) const actual = fss(fixture) - assert.is(actual, expected) + assert.equal(actual, expected) assert.end() }) @@ -23,50 +21,54 @@ test('circular getter reference to root', function (assert) { } } - const expected = s( - { circle: '[Circular]', name: 'Tywin Lannister' } - ) + const expected = s({ circle: '[Circular]', name: 'Tywin Lannister' }) const actual = fss(fixture) - assert.is(actual, expected) + assert.equal(actual, expected) assert.end() }) test('nested circular reference to root', function (assert) { const fixture = { name: 'Tywin Lannister' } fixture.id = { circle: fixture } - const expected = s( - { id: { circle: '[Circular]' }, name: 'Tywin Lannister' } - ) + const expected = s({ id: { circle: '[Circular]' }, name: 'Tywin Lannister' }) const actual = fss(fixture) - assert.is(actual, expected) + assert.equal(actual, expected) assert.end() }) test('child circular reference', function (assert) { - const fixture = { name: 'Tywin Lannister', child: { name: 'Tyrion Lannister' } } + const fixture = { + name: 'Tywin Lannister', + child: { name: 'Tyrion Lannister' } + } fixture.child.dinklage = fixture.child const expected = s({ child: { - dinklage: '[Circular]', name: 'Tyrion Lannister' + dinklage: '[Circular]', + name: 'Tyrion Lannister' }, name: 'Tywin Lannister' }) const actual = fss(fixture) - assert.is(actual, expected) + assert.equal(actual, expected) assert.end() }) test('nested child circular reference', function (assert) { - const fixture = { name: 'Tywin Lannister', child: { name: 'Tyrion Lannister' } } + const fixture = { + name: 'Tywin Lannister', + child: { name: 'Tyrion Lannister' } + } fixture.child.actor = { dinklage: fixture.child } const expected = s({ child: { - actor: { dinklage: '[Circular]' }, name: 'Tyrion Lannister' + actor: { dinklage: '[Circular]' }, + name: 'Tyrion Lannister' }, name: 'Tywin Lannister' }) const actual = fss(fixture) - assert.is(actual, expected) + assert.equal(actual, expected) assert.end() }) @@ -74,10 +76,11 @@ test('circular objects in an array', function (assert) { const fixture = { name: 'Tywin Lannister' } fixture.hand = [fixture, fixture] const expected = s({ - hand: ['[Circular]', '[Circular]'], name: 'Tywin Lannister' + hand: ['[Circular]', '[Circular]'], + name: 'Tywin Lannister' }) const actual = fss(fixture) - assert.is(actual, expected) + assert.equal(actual, expected) assert.end() }) @@ -97,7 +100,7 @@ test('nested circular references in an array', function (assert) { ] }) const actual = fss(fixture) - assert.is(actual, expected) + assert.equal(actual, expected) assert.end() }) @@ -106,7 +109,7 @@ test('circular arrays', function (assert) { fixture.push(fixture, fixture) const expected = s(['[Circular]', '[Circular]']) const actual = fss(fixture) - assert.is(actual, expected) + assert.equal(actual, expected) assert.end() }) @@ -121,7 +124,7 @@ test('nested circular arrays', function (assert) { { bastards: '[Circular]', name: 'Ramsay Bolton' } ]) const actual = fss(fixture) - assert.is(actual, expected) + assert.equal(actual, expected) assert.end() }) @@ -133,7 +136,7 @@ test('repeated non-circular references in objects', function (assert) { } const expected = s(fixture) const actual = fss(fixture) - assert.is(actual, expected) + assert.equal(actual, expected) assert.end() }) @@ -142,7 +145,7 @@ test('repeated non-circular references in arrays', function (assert) { const fixture = [daenerys, daenerys] const expected = s(fixture) const actual = fss(fixture) - assert.is(actual, expected) + assert.equal(actual, expected) assert.end() }) @@ -156,24 +159,28 @@ test('double child circular reference', function (assert) { const cloned = clone(fixture) const expected = s({ childA: { - dinklage: '[Circular]', name: 'Tyrion Lannister' + dinklage: '[Circular]', + name: 'Tyrion Lannister' }, childB: { - dinklage: '[Circular]', name: 'Tyrion Lannister' + dinklage: '[Circular]', + name: 'Tyrion Lannister' }, name: 'Tywin Lannister' }) const actual = fss(fixture) - assert.is(actual, expected) + assert.equal(actual, expected) // check if the fixture has not been modified - assert.deepEqual(fixture, cloned) + assert.same(fixture, cloned) assert.end() }) test('child circular reference with toJSON', function (assert) { - // Create a test object that has an overriden `toJSON` property - TestObject.prototype.toJSON = function () { return { special: 'case' } } + // Create a test object that has an overridden `toJSON` property + TestObject.prototype.toJSON = function () { + return { special: 'case' } + } function TestObject (content) {} // Creating a simple circular object structure @@ -187,16 +194,22 @@ test('child circular reference with toJSON', function (assert) { otherParentObject.otherChildObject.otherParentObject = otherParentObject // Making sure our original tests work - assert.deepEqual(parentObject.childObject.parentObject, parentObject) - assert.deepEqual(otherParentObject.otherChildObject.otherParentObject, otherParentObject) + assert.same(parentObject.childObject.parentObject, parentObject) + assert.same( + otherParentObject.otherChildObject.otherParentObject, + otherParentObject + ) // Should both be idempotent assert.equal(fss(parentObject), '{"childObject":{"special":"case"}}') assert.equal(fss(otherParentObject), '{"special":"case"}') // Therefore the following assertion should be `true` - assert.deepEqual(parentObject.childObject.parentObject, parentObject) - assert.deepEqual(otherParentObject.otherChildObject.otherParentObject, otherParentObject) + assert.same(parentObject.childObject.parentObject, parentObject) + assert.same( + otherParentObject.otherChildObject.otherParentObject, + otherParentObject + ) assert.end() }) @@ -204,14 +217,14 @@ test('child circular reference with toJSON', function (assert) { test('null object', function (assert) { const expected = s(null) const actual = fss(null) - assert.is(actual, expected) + assert.equal(actual, expected) assert.end() }) test('null property', function (assert) { const expected = s({ f: null }) const actual = fss({ f: null }) - assert.is(actual, expected) + assert.equal(actual, expected) assert.end() }) @@ -258,7 +271,7 @@ test('nested child circular reference in toJSON', function (assert) { } }) const actual = fss(o) - assert.is(actual, expected) + assert.equal(actual, expected) assert.end() }) @@ -271,7 +284,7 @@ test('circular getters are restored when stringified', function (assert) { } fss(fixture) - assert.is(fixture.circle, fixture) + assert.equal(fixture.circle, fixture) assert.end() }) @@ -279,13 +292,15 @@ test('non-configurable circular getters use a replacer instead of markers', func const fixture = { name: 'Tywin Lannister' } Object.defineProperty(fixture, 'circle', { configurable: false, - get: function () { return fixture }, + get: function () { + return fixture + }, enumerable: true }) fss(fixture) - assert.is(fixture.circle, fixture) + assert.equal(fixture.circle, fixture) assert.end() }) @@ -294,20 +309,25 @@ test('getter child circular reference', function (assert) { name: 'Tywin Lannister', child: { name: 'Tyrion Lannister', - get dinklage () { return fixture.child } + get dinklage () { + return fixture.child + } }, - get self () { return fixture } + get self () { + return fixture + } } const expected = s({ child: { - dinklage: '[Circular]', name: 'Tyrion Lannister' + dinklage: '[Circular]', + name: 'Tyrion Lannister' }, name: 'Tywin Lannister', self: '[Circular]' }) const actual = fss(fixture) - assert.is(actual, expected) + assert.equal(actual, expected) assert.end() }) @@ -318,6 +338,67 @@ test('Proxy throwing', function (assert) { s.write('', () => { assert.end() }) - const actual = fss({ s, p: new Proxy({}, { get() { throw new Error('kaboom') } }) }) - assert.is(actual, '"[unable to serialize, circular reference is too complex to analyze]"') + const actual = fss({ s, p: new Proxy({}, { get () { throw new Error('kaboom') } }) }) + assert.equal(actual, '"[unable to serialize, circular reference is too complex to analyze]"') +}) + +test('depthLimit option - will replace deep objects', function (assert) { + const fixture = { + name: 'Tywin Lannister', + child: { + name: 'Tyrion Lannister' + }, + get self () { + return fixture + } + } + + const expected = s({ + child: '[...]', + name: 'Tywin Lannister', + self: '[Circular]' + }) + const actual = fss(fixture, undefined, undefined, { + depthLimit: 1, + edgesLimit: 1 + }) + assert.equal(actual, expected) + assert.end() +}) + +test('edgesLimit option - will replace deep objects', function (assert) { + const fixture = { + object: { + 1: { test: 'test' }, + 2: { test: 'test' }, + 3: { test: 'test' }, + 4: { test: 'test' } + }, + array: [ + { test: 'test' }, + { test: 'test' }, + { test: 'test' }, + { test: 'test' } + ], + get self () { + return fixture + } + } + + const expected = s({ + array: [{ test: 'test' }, { test: 'test' }, { test: 'test' }, '[...]'], + object: { + 1: { test: 'test' }, + 2: { test: 'test' }, + 3: { test: 'test' }, + 4: '[...]' + }, + self: '[Circular]' + }) + const actual = fss(fixture, undefined, undefined, { + depthLimit: 3, + edgesLimit: 3 + }) + assert.equal(actual, expected) + assert.end() }) diff --git a/test.js b/test.js index ffef1d3..a4170e9 100644 --- a/test.js +++ b/test.js @@ -7,11 +7,9 @@ const stream = require('stream') test('circular reference to root', function (assert) { const fixture = { name: 'Tywin Lannister' } fixture.circle = fixture - const expected = s( - { name: 'Tywin Lannister', circle: '[Circular]' } - ) + const expected = s({ name: 'Tywin Lannister', circle: '[Circular]' }) const actual = fss(fixture) - assert.is(actual, expected) + assert.equal(actual, expected) assert.end() }) @@ -22,50 +20,54 @@ test('circular getter reference to root', function (assert) { return fixture } } - const expected = s( - { name: 'Tywin Lannister', circle: '[Circular]' } - ) + const expected = s({ name: 'Tywin Lannister', circle: '[Circular]' }) const actual = fss(fixture) - assert.is(actual, expected) + assert.equal(actual, expected) assert.end() }) test('nested circular reference to root', function (assert) { const fixture = { name: 'Tywin Lannister' } fixture.id = { circle: fixture } - const expected = s( - { name: 'Tywin Lannister', id: { circle: '[Circular]' } } - ) + const expected = s({ name: 'Tywin Lannister', id: { circle: '[Circular]' } }) const actual = fss(fixture) - assert.is(actual, expected) + assert.equal(actual, expected) assert.end() }) test('child circular reference', function (assert) { - const fixture = { name: 'Tywin Lannister', child: { name: 'Tyrion Lannister' } } + const fixture = { + name: 'Tywin Lannister', + child: { name: 'Tyrion Lannister' } + } fixture.child.dinklage = fixture.child const expected = s({ name: 'Tywin Lannister', child: { - name: 'Tyrion Lannister', dinklage: '[Circular]' + name: 'Tyrion Lannister', + dinklage: '[Circular]' } }) const actual = fss(fixture) - assert.is(actual, expected) + assert.equal(actual, expected) assert.end() }) test('nested child circular reference', function (assert) { - const fixture = { name: 'Tywin Lannister', child: { name: 'Tyrion Lannister' } } + const fixture = { + name: 'Tywin Lannister', + child: { name: 'Tyrion Lannister' } + } fixture.child.actor = { dinklage: fixture.child } const expected = s({ name: 'Tywin Lannister', child: { - name: 'Tyrion Lannister', actor: { dinklage: '[Circular]' } + name: 'Tyrion Lannister', + actor: { dinklage: '[Circular]' } } }) const actual = fss(fixture) - assert.is(actual, expected) + assert.equal(actual, expected) assert.end() }) @@ -73,10 +75,11 @@ test('circular objects in an array', function (assert) { const fixture = { name: 'Tywin Lannister' } fixture.hand = [fixture, fixture] const expected = s({ - name: 'Tywin Lannister', hand: ['[Circular]', '[Circular]'] + name: 'Tywin Lannister', + hand: ['[Circular]', '[Circular]'] }) const actual = fss(fixture) - assert.is(actual, expected) + assert.equal(actual, expected) assert.end() }) @@ -96,7 +99,7 @@ test('nested circular references in an array', function (assert) { ] }) const actual = fss(fixture) - assert.is(actual, expected) + assert.equal(actual, expected) assert.end() }) @@ -105,7 +108,7 @@ test('circular arrays', function (assert) { fixture.push(fixture, fixture) const expected = s(['[Circular]', '[Circular]']) const actual = fss(fixture) - assert.is(actual, expected) + assert.equal(actual, expected) assert.end() }) @@ -120,7 +123,7 @@ test('nested circular arrays', function (assert) { { name: 'Ramsay Bolton', bastards: '[Circular]' } ]) const actual = fss(fixture) - assert.is(actual, expected) + assert.equal(actual, expected) assert.end() }) @@ -132,7 +135,7 @@ test('repeated non-circular references in objects', function (assert) { } const expected = s(fixture) const actual = fss(fixture) - assert.is(actual, expected) + assert.equal(actual, expected) assert.end() }) @@ -141,7 +144,7 @@ test('repeated non-circular references in arrays', function (assert) { const fixture = [daenerys, daenerys] const expected = s(fixture) const actual = fss(fixture) - assert.is(actual, expected) + assert.equal(actual, expected) assert.end() }) @@ -156,23 +159,27 @@ test('double child circular reference', function (assert) { const expected = s({ name: 'Tywin Lannister', childA: { - name: 'Tyrion Lannister', dinklage: '[Circular]' + name: 'Tyrion Lannister', + dinklage: '[Circular]' }, childB: { - name: 'Tyrion Lannister', dinklage: '[Circular]' + name: 'Tyrion Lannister', + dinklage: '[Circular]' } }) const actual = fss(fixture) - assert.is(actual, expected) + assert.equal(actual, expected) // check if the fixture has not been modified - assert.deepEqual(fixture, cloned) + assert.same(fixture, cloned) assert.end() }) test('child circular reference with toJSON', function (assert) { - // Create a test object that has an overriden `toJSON` property - TestObject.prototype.toJSON = function () { return { special: 'case' } } + // Create a test object that has an overridden `toJSON` property + TestObject.prototype.toJSON = function () { + return { special: 'case' } + } function TestObject (content) {} // Creating a simple circular object structure @@ -186,16 +193,22 @@ test('child circular reference with toJSON', function (assert) { otherParentObject.otherChildObject.otherParentObject = otherParentObject // Making sure our original tests work - assert.deepEqual(parentObject.childObject.parentObject, parentObject) - assert.deepEqual(otherParentObject.otherChildObject.otherParentObject, otherParentObject) + assert.same(parentObject.childObject.parentObject, parentObject) + assert.same( + otherParentObject.otherChildObject.otherParentObject, + otherParentObject + ) // Should both be idempotent assert.equal(fss(parentObject), '{"childObject":{"special":"case"}}') assert.equal(fss(otherParentObject), '{"special":"case"}') // Therefore the following assertion should be `true` - assert.deepEqual(parentObject.childObject.parentObject, parentObject) - assert.deepEqual(otherParentObject.otherChildObject.otherParentObject, otherParentObject) + assert.same(parentObject.childObject.parentObject, parentObject) + assert.same( + otherParentObject.otherChildObject.otherParentObject, + otherParentObject + ) assert.end() }) @@ -203,14 +216,14 @@ test('child circular reference with toJSON', function (assert) { test('null object', function (assert) { const expected = s(null) const actual = fss(null) - assert.is(actual, expected) + assert.equal(actual, expected) assert.end() }) test('null property', function (assert) { const expected = s({ f: null }) const actual = fss({ f: null }) - assert.is(actual, expected) + assert.equal(actual, expected) assert.end() }) @@ -251,7 +264,7 @@ test('nested child circular reference in toJSON', function (assert) { } }) const actual = fss(o) - assert.is(actual, expected) + assert.equal(actual, expected) assert.end() }) @@ -264,7 +277,7 @@ test('circular getters are restored when stringified', function (assert) { } fss(fixture) - assert.is(fixture.circle, fixture) + assert.equal(fixture.circle, fixture) assert.end() }) @@ -272,13 +285,15 @@ test('non-configurable circular getters use a replacer instead of markers', func const fixture = { name: 'Tywin Lannister' } Object.defineProperty(fixture, 'circle', { configurable: false, - get: function () { return fixture }, + get: function () { + return fixture + }, enumerable: true }) fss(fixture) - assert.is(fixture.circle, fixture) + assert.equal(fixture.circle, fixture) assert.end() }) @@ -287,20 +302,25 @@ test('getter child circular reference are replaced instead of marked', function name: 'Tywin Lannister', child: { name: 'Tyrion Lannister', - get dinklage () { return fixture.child } + get dinklage () { + return fixture.child + } }, - get self () { return fixture } + get self () { + return fixture + } } const expected = s({ name: 'Tywin Lannister', child: { - name: 'Tyrion Lannister', dinklage: '[Circular]' + name: 'Tyrion Lannister', + dinklage: '[Circular]' }, self: '[Circular]' }) const actual = fss(fixture) - assert.is(actual, expected) + assert.equal(actual, expected) assert.end() }) @@ -311,6 +331,67 @@ test('Proxy throwing', function (assert) { s.write('', () => { assert.end() }) - const actual = fss({ s, p: new Proxy({}, { get() { throw new Error('kaboom') } }) }) - assert.is(actual, '"[unable to serialize, circular reference is too complex to analyze]"') + const actual = fss({ s, p: new Proxy({}, { get () { throw new Error('kaboom') } }) }) + assert.equal(actual, '"[unable to serialize, circular reference is too complex to analyze]"') +}) + +test('depthLimit option - will replace deep objects', function (assert) { + const fixture = { + name: 'Tywin Lannister', + child: { + name: 'Tyrion Lannister' + }, + get self () { + return fixture + } + } + + const expected = s({ + name: 'Tywin Lannister', + child: '[...]', + self: '[Circular]' + }) + const actual = fss(fixture, undefined, undefined, { + depthLimit: 1, + edgesLimit: 1 + }) + assert.equal(actual, expected) + assert.end() +}) + +test('edgesLimit option - will replace deep objects', function (assert) { + const fixture = { + object: { + 1: { test: 'test' }, + 2: { test: 'test' }, + 3: { test: 'test' }, + 4: { test: 'test' } + }, + array: [ + { test: 'test' }, + { test: 'test' }, + { test: 'test' }, + { test: 'test' } + ], + get self () { + return fixture + } + } + + const expected = s({ + object: { + 1: { test: 'test' }, + 2: { test: 'test' }, + 3: { test: 'test' }, + 4: '[...]' + }, + array: [{ test: 'test' }, { test: 'test' }, { test: 'test' }, '[...]'], + self: '[Circular]' + }) + const actual = fss(fixture, undefined, undefined, { + depthLimit: 3, + edgesLimit: 3 + }) + assert.equal(actual, expected) + assert.end() })