UNPKG

jsan

Version:

handle circular references when stringifying and parsing

450 lines (386 loc) 15.4 kB
var assert = require('assert'); var jsan = require('../'); describe('jsan', function() { describe('has a stringify method', function() { it('respects the replacer array argument', function() { var obj = {a: 1, b: 2}; var JSONed = JSON.stringify(obj, ['a']); var jsaned = jsan.stringify(obj, ['a']); assert.equal(JSONed, jsaned); }); it('respects the replacer function argument', function() { var obj = {a: 1, b: 2, c: {r: /foo/}}; var replacer = function(index, value) { if (value.test) { return value.toString(); } } var JSONed = JSON.stringify(obj, replacer); var jsaned = jsan.stringify(obj, replacer); assert.equal(JSONed, jsaned); }); it('respects the space argument', function() { var obj = {a: 1, b: 2, c: {foo: 'bar'}}; var JSONed = JSON.stringify(obj, null, 2); var jsaned = jsan.stringify(obj, null, 2); assert.equal(JSONed, jsaned); }); it('behaves the same as JSON.stringify for simple jsonable objects', function() { var obj = { a: 1, b: 'string', c: [2,3], d: null }; assert.equal(JSON.stringify(obj), jsan.stringify(obj)); }); it('uses the toJSON() method when possible', function() { var obj = { a: { b: 1, toJSON: function(key) { return key } } }; assert.equal(jsan.stringify(obj, null, null, false), '{"a":"a"}'); }); it('can handle dates', function() { var obj = { now: new Date() } var str = jsan.stringify(obj, null, null, true); assert(/^\{"now":\{"\$jsan":"d[^"]*"\}\}$/.test(str)); }); it('can handle regexes', function() { var obj = { r: /test/ } var str = jsan.stringify(obj, null, null, true); assert.deepEqual(str, '{"r":{"$jsan":"r,test"}}'); }); it('can handle functions', function() { var obj = { f: function () {} } var str = jsan.stringify(obj, null, null, true); assert.deepEqual(str, '{"f":{"$jsan":"ffunction () { /* ... */ }"}}'); }); it('can handle undefined', function() { var obj = undefined; var str = jsan.stringify(obj, null, null, true); assert.deepEqual(str, '{"$jsan":"u"}'); }); it('can handle NaN', function() { var obj = NaN; var str = jsan.stringify(obj, null, null, true); assert.deepEqual(str, '{"$jsan":"n"}'); }); it('can handle Infinity', function() { var obj = Infinity; var str = jsan.stringify(obj, null, null, true); assert.deepEqual(str, '{"$jsan":"i"}'); }); it('can handle -Infinity', function() { var obj = -Infinity; var str = jsan.stringify(obj, null, null, true); assert.deepEqual(str, '{"$jsan":"y"}'); }); it('can handle nested undefined', function() { var obj = { u: undefined } var str = jsan.stringify(obj, null, null, true); assert.deepEqual(str, '{"u":{"$jsan":"u"}}'); }); it('can handle nested NaN', function() { var obj = { u: NaN } var str = jsan.stringify(obj, null, null, true); assert.deepEqual(str, '{"u":{"$jsan":"n"}}'); }); it('can handle nested Infinity', function() { var obj = { u: Infinity } var str = jsan.stringify(obj, null, null, true); assert.deepEqual(str, '{"u":{"$jsan":"i"}}'); }); it('can handle nested -Infinity', function() { var obj = { u: -Infinity } var str = jsan.stringify(obj, null, null, true); assert.deepEqual(str, '{"u":{"$jsan":"y"}}'); }); it('can handle errors', function() { var obj = { e: new Error(':(') } var str = jsan.stringify(obj, null, null, true); assert.deepEqual(str, '{"e":{"$jsan":"e:("}}'); }); if (typeof Symbol !== 'undefined') { it('can handle ES symbols', function() { var obj = { s: Symbol('a') } var str = jsan.stringify(obj, null, null, true); assert.deepEqual(str, '{"s":{"$jsan":"sa"}}'); }); it('can handle global ES symbols', function() { var obj = { g: Symbol.for('a') } var str = jsan.stringify(obj, null, null, true); assert.deepEqual(str, '{"g":{"$jsan":"ga"}}'); }); } if (typeof Map !== 'undefined' && typeof Array.from !== 'undefined') { it('can handle ES Map', function() { var obj = { map: new Map([ ['a', 1], [{toString: function (){ return 'a' }}, 2], [{}, 3] ]) } var str = jsan.stringify(obj, null, null, true); assert.deepEqual(str, '{"map":{"$jsan":"m[[\\"a\\",1],[{\\"toString\\":{\\"$jsan\\":\\"ffunction (){ /* ... */ }\\"}},2],[{},3]]"}}'); }); } if (typeof Set !== 'undefined' && typeof Array.from !== 'undefined') { it('can handle ES Set', function() { var obj = { set: new Set(['a', {toString: function (){ return 'a' }}, {}]) } var str = jsan.stringify(obj, null, null, true); assert.deepEqual(str, '{"set":{"$jsan":"l[\\"a\\",{\\"toString\\":{\\"$jsan\\":\\"ffunction (){ /* ... */ }\\"}},{}]"}}'); }); } it('works on objects with circular references', function() { var obj = {}; obj['self'] = obj; assert.equal(jsan.stringify(obj), '{"self":{"$jsan":"$"}}'); }); it('can use the circular option', function() { var obj = {}; obj.self = obj; obj.a = 1; obj.b = {}; obj.c = obj.b; assert.equal(jsan.stringify(obj, null, null, {circular: ''}), '{"self":"∞","a":1,"b":{},"c":{"$jsan":"$.b"}}'); assert.equal(jsan.stringify(obj, null, null, {circular: function() { return '∞!' }}), '{"self":"∞!","a":1,"b":{},"c":{"$jsan":"$.b"}}'); }); it('can use the refs option', function() { var obj1 = { a: 1 }; var obj = { "prop1": obj1, "prop2": { "prop3": obj1 } }; assert.equal(jsan.stringify(obj, null, null, {refs: true}), '{"prop1":{"a":1},"prop2":{"prop3":{"$jsan":"$.prop1"}}}'); assert.equal(jsan.stringify(obj, null, null, true), '{"prop1":{"a":1},"prop2":{"prop3":{"$jsan":"$.prop1"}}}'); assert.equal(jsan.stringify(obj, null, null, false), '{"prop1":{"a":1},"prop2":{"prop3":{"$jsan":"$.prop1"}}}'); assert.equal(jsan.stringify(obj, null, null, {refs: false}), '{"prop1":{"a":1},"prop2":{"prop3":{"a":1}}}'); }); it('works with refs option with circular references', function() { var obj = {}; obj.self = obj; obj.a = 1; obj.b = {t: 1}; obj.c = obj.b; assert.equal(jsan.stringify(obj, null, null, {refs: false}), '{"self":{"$jsan":"$"},"a":1,"b":{"t":1},"c":{"t":1}}'); assert.equal(jsan.stringify(obj, null, null, {refs: false, circular: 'Circular'}), '{"self":"Circular","a":1,"b":{"t":1},"c":{"t":1}}'); }); it('works on objects with "[", "\'", and "]" in the keys', function() { var obj = {}; obj['["key"]'] = {}; obj['["key"]']['["key"]'] = obj['["key"]']; assert.equal(jsan.stringify(obj), '{"[\\"key\\"]":{"[\\"key\\"]":{"$jsan":"$[\\"[\\\\\\"key\\\\\\"]\\"]"}}}'); }); it('works on objects that will get encoded with \\uXXXX', function() { var obj = {"\u017d\u010d":{},"kraj":"\u017du\u017e"}; obj["\u017d\u010d"]["\u017d\u010d"] = obj["\u017d\u010d"]; assert.equal(jsan.stringify(obj), '{"\u017d\u010d":{"\u017d\u010d":{"$jsan":"$[\\\"\u017d\u010d\\\"]"}},"kraj":"Žuž"}'); }); it('works on circular arrays', function() { var obj = []; obj[0] = []; obj[0][0] = obj[0]; assert.equal(jsan.stringify(obj), '[[{"$jsan":"$[0]"}]]'); }); it('works correctly for mutiple calls with the same object', function() { var obj = {}; obj.self = obj; obj.a = {}; obj.b = obj.a; assert.equal(jsan.stringify(obj), '{"self":{"$jsan":"$"},"a":{},"b":{"$jsan":"$.a"}}'); assert.equal(jsan.stringify(obj), '{"self":{"$jsan":"$"},"a":{},"b":{"$jsan":"$.a"}}'); }); it('does not report false positives for circular references', function() { /** * This is a test for an edge case in which jsan.stringify falsely reported circular references * The minimal conditions are * 1) The object has an error preventing serialization by json.stringify * 2) The object contains a repeated reference * 3) the second path of the reference contains the first path */ var circular = {}; circular.self = circular; var ref = {}; var obj = { circularRef: circular, ref: ref, refAgain: ref, }; var result = jsan.stringify(obj, null, null, { circular: "[CIRCULAR]" }); assert.equal('{"circularRef":{"self":"[CIRCULAR]"},"ref":{},"refAgain":{"$jsan":"$.ref"}}', result) }); }); describe('has a parse method', function() { it('behaves the same as JSON.parse for valid json strings', function() { var str = '{"a":1,"b":"string","c":[2,3],"d":null}'; assert.deepEqual(JSON.parse(str), jsan.parse(str)); }); it('uses reviver function', function() { var str = '{"a":1,"b":null}'; assert.deepEqual({"a":1,"b":2}, jsan.parse(str, function (key, value) { return key === "b" ? 2 : value; }) ); }); it('can decode dates', function() { var str = '{"$jsan":"d1400000000000"}'; var obj = jsan.parse(str); assert(obj instanceof Date); }); it('can decode dates while using reviver', function() { var str = '{"$jsan":"d1400000000000"}'; var obj = jsan.parse(str, function (key, value) { return value; }); assert(obj instanceof Date); }); it('can decode regexes', function() { str = '{"$jsan":"r,test"}'; var obj = jsan.parse(str); assert(obj instanceof RegExp ) }); it('can decode functions', function() { str = '{"$jsan":"ffunction () { /* ... */ }"}'; var obj = jsan.parse(str); assert(obj instanceof Function); }); it('can decode undefined', function() { str = '{"$jsan":"u"}'; var obj = jsan.parse(str); assert(obj === undefined); }); it('can decode NaN', function() { str = '{"$jsan":"n"}'; var obj = jsan.parse(str); assert(isNaN(obj) && typeof obj === 'number'); }); it('can decode Infinity', function() { str = '{"$jsan":"i"}'; var obj = jsan.parse(str); assert(obj === Number.POSITIVE_INFINITY); }); it('can decode -Infinity', function() { str = '{"$jsan":"y"}'; var obj = jsan.parse(str); assert(obj === Number.NEGATIVE_INFINITY); }); it('can decode errors', function() { str = '{"$jsan":"e:("}'; var obj = jsan.parse(str); assert(obj instanceof Error && obj.message === ':('); }); it('can decode nested dates', function() { var str = '{"now":{"$jsan":"d1400000000000"}}'; var obj = jsan.parse(str); assert(obj.now instanceof Date); }); it('can decode nested regexes', function() { str = '{"r":{"$jsan":"r,test"}}'; var obj = jsan.parse(str); assert(obj.r instanceof RegExp ) }); it('can decode nested functions', function() { str = '{"f":{"$jsan":"ffunction () { /* ... */ }"}}'; var obj = jsan.parse(str); assert(obj.f instanceof Function); }); it('can decode nested undefined', function() { str = '{"u":{"$jsan":"u"}}'; var obj = jsan.parse(str); assert('u' in obj && obj.u === undefined); }); it('can decode nested NaN', function() { str = '{"u":{"$jsan":"n"}}'; var obj = jsan.parse(str); assert('u' in obj && isNaN(obj.u) && typeof obj.u === 'number'); }); it('can decode nested Infinity', function() { str = '{"u":{"$jsan":"i"}}'; var obj = jsan.parse(str); assert('u' in obj && obj.u === Number.POSITIVE_INFINITY); }); it('can decode nested -Infinity', function() { str = '{"u":{"$jsan":"y"}}'; var obj = jsan.parse(str); assert('u' in obj && obj.u === Number.NEGATIVE_INFINITY); }); it('can decode nested errors', function() { str = '{"e":{"$jsan":"e:("}}'; var obj = jsan.parse(str); assert(obj.e instanceof Error && obj.e.message === ':('); }); if (typeof Symbol !== 'undefined') { it('can decode ES symbols', function() { str = '{"s1":{"$jsan":"sfoo"}, "s2":{"$jsan":"s"}}'; var obj = jsan.parse(str); assert(typeof obj.s1 === 'symbol' && obj.s1.toString() === 'Symbol(foo)'); assert(typeof obj.s2 === 'symbol' && obj.s2.toString() === 'Symbol()'); }); it('can decode global ES symbols', function() { str = '{"g1":{"$jsan":"gfoo"}, "g2":{"$jsan":"gundefined"}}'; var obj = jsan.parse(str); assert(typeof obj.g1 === 'symbol' && obj.g1 === Symbol.for('foo')); assert(typeof obj.g2 === 'symbol' && obj.g2 === Symbol.for()); }); } if (typeof Map !== 'undefined' && typeof Array.from !== 'undefined') { it('can decode ES Map', function() { var str = '{"map":{"$jsan":"m[[\\"a\\",1],[{\\"toString\\":{\\"$jsan\\":\\"ffunction (){ /* ... */ }\\"}},2],[{},3]]"}}'; var obj = jsan.parse(str); var keys = obj.map.keys(); var values = obj.map.values(); assert.equal(keys.next().value, 'a'); assert.equal(typeof keys.next().value.toString, 'function'); assert.equal(typeof keys.next().value, 'object'); assert.equal(values.next().value, 1); assert.equal(values.next().value, 2); assert.equal(values.next().value, 3); }); } if (typeof Set !== 'undefined' && typeof Array.from !== 'undefined') { it('can decode ES Set', function() { var str = '{"set":{"$jsan":"l[\\"a\\",{\\"toString\\":{\\"$jsan\\":\\"ffunction (){ /* ... */ }\\"}},{}]"}}'; var obj = jsan.parse(str); var values = obj.set.values(); assert.equal(values.next().value, 'a'); assert.equal(typeof values.next().value.toString, 'function'); assert.equal(typeof values.next().value, 'object'); }); } it('works on object strings with a circular dereferences', function() { var str = '{"a":1,"b":"string","c":[2,3],"d":null,"self":{"$jsan":"$"}}'; var obj = jsan.parse(str); assert.deepEqual(obj['self'], obj); }); it('works on object strings with "[", "\'", and "]" in the keys', function() { var str = '{"[\\"key\\"]":{"[\\"key\\"]":{"$jsan":"$[\\"[\\\\\\"key\\\\\\"]\\"]"}}}'; var obj = jsan.parse(str); assert.deepEqual(obj['["key"]']['["key"]'], obj['["key"]']); }); it('works on objects encoded with \\uXXXX', function() { var str = '{"\u017d\u010d":{"\u017d\u010d":{"$jsan":"$[\\\"\\u017d\\u010d\\\"]"}},"kraj":"Žuž"}'; var obj = jsan.parse(str); assert.deepEqual(obj["\u017d\u010d"]["\u017d\u010d"], obj["\u017d\u010d"]); }); it('works on array strings with circular dereferences', function() { var str = '[[{"$jsan":"$[0]"}]]'; var arr = jsan.parse(str); assert.deepEqual(arr[0][0], arr[0]); }); }); });