diff --git a/benchmark.js b/benchmark.js index 58145e3..0d3dc17 100644 --- a/benchmark.js +++ b/benchmark.js @@ -59,6 +59,33 @@ suite.add('fast-safe-stringify: deep circular', function () { fastSafeStringify(deepCirc) }) +suite.add('\ndecycle: simple object', function () { + fastSafeStringify.decycle(obj) +}) +suite.add('decycle: circular ', function () { + fastSafeStringify.decycle(circ) +}) +suite.add('decycle: deep ', function () { + fastSafeStringify.decycle(deep) +}) +suite.add('decycle: deep circular', function () { + fastSafeStringify.decycle(deepCirc) +}) + +const replacer = (_val, k) => k +suite.add('\ndecycle with custom replacer: simple object', function () { + fastSafeStringify.decycle(obj, replacer) +}) +suite.add('decycle with custom replacer: circular ', function () { + fastSafeStringify.decycle(circ, replacer) +}) +suite.add('decycle with custom replacer: deep ', function () { + fastSafeStringify.decycle(deep, replacer) +}) +suite.add('decycle with custom replacer: deep circular', function () { + fastSafeStringify.decycle(deepCirc, replacer) +}) + // add listeners suite.on('cycle', function (event) { console.log(String(event.target)) diff --git a/index.d.ts b/index.d.ts index a842bf3..ede560e 100644 --- a/index.d.ts +++ b/index.d.ts @@ -3,6 +3,7 @@ declare function stringify(data: any): string; declare namespace stringify { export function stable(data: any): string; export function stableStringify(data: any): string; + export function decycle(val: any, replacer?: (val: any, k: string, stack: [any, any][], parent?: any) => any | void): any; } -export default stringify; +export default stringify; \ No newline at end of file diff --git a/index.js b/index.js index 6fe35f3..532e621 100644 --- a/index.js +++ b/index.js @@ -2,6 +2,7 @@ module.exports = stringify stringify.default = stringify stringify.stable = deterministicStringify stringify.stableStringify = deterministicStringify +stringify.decycle = decycle const arr = [] @@ -15,27 +16,39 @@ function stringify (obj, replacer, spacer) { } return res } -function decirc (val, k, stack, parent) { + +function decycle (val, replacer) { + decirc(val, '', [], undefined, replacer) + return val +} + +function defaultReplacer (val, parentKey, stack, parent) { + arr.push([parent, parentKey, val]) + return '[Circular]' +} + +function decirc (val, parentKey, stack, parent, replacer) { + replacer = replacer || defaultReplacer + var i if (typeof val === 'object' && val !== null) { for (i = 0; i < stack.length; i++) { - if (stack[i] === val) { - parent[k] = '[Circular]' - arr.push([parent, k, val]) + if (stack[i][0] === val) { + parent[parentKey] = replacer(val, parentKey, stack, parent) return } } - stack.push(val) + stack.push([val, parentKey]) // 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, stack, val, replacer) } } else { const 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, stack, val, replacer) } } stack.pop() diff --git a/test.js b/test.js index 0b0cdf3..f38ebe3 100644 --- a/test.js +++ b/test.js @@ -238,3 +238,78 @@ test('nested child circular reference in toJSON', function (assert) { assert.is(actual, expected) assert.end() }) + +// this custom replacer will add a json pointer $ref pointing to the referenced js object +test('decycle supports custom replacer', function (assert) { + const fixture = { + definitions: { + Customer: { + properties: { + partners: { + items: {} + } + } + }, + Partner: { + properties: { + customers: { + items: {} + } + } + } + } + } + + fixture.definitions.Customer.properties.partners.items = fixture.definitions.Partner + fixture.definitions.Partner.properties.customers.items = fixture.definitions.Customer + + const expected = { + definitions: { + Customer: { + properties: { + partners: { + items: { + properties: { + customers: { + items: { + $ref: '#/definitions/Customer' + } + } + } + } + } + } + }, + Partner: { + properties: { + customers: { + items: { + $ref: '#/definitions/Customer' + } + } + } + } + } + } + + const actual = fss.decycle(fixture, (val, k, stack, parent) => { + let $ref = '#' + + for (let i = 0; i < stack.length; i++) { + if (stack[i][1]) { + $ref += `/${stack[i][1]}` + } + + if (stack[i][0] === val) { + break + } + } + + return { + $ref + } + }) + + assert.deepEqual(actual, expected) + assert.end() +})