package/.editorconfig000644 0000000305 3560116604 011733 0ustar00000000 000000 # http://editorconfig.org root = true [*] indent_style = space indent_size = 2 charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true [*.md] trim_trailing_whitespace = false package/LICENSE000644 0000002041 3560116604 010262 0ustar00000000 000000 Copyright (c) 2018 Moshe Kolodny Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. package/perf/bench.js000644 0000003244 3560116604 011634 0ustar00000000 000000 var jsan = require('../'); var decycle = require('../lib/cycle').decycle; var CircularJSON = require('circular-json'); var stringify = require('json-stringify-safe'); var Benchmark = require('benchmark'); var decycledGlobal = decycle(global, {}); const suite = (name, obj) => () => { return new Promise(function(resolve) { var hzs = [] console.log(name) new Benchmark.Suite(name) .add('jsan', () => jsan.stringify(obj)) .add('CircularJSON', () => CircularJSON.stringify(obj)) .add('json-stringify-safe', () => stringify(obj)) .on('cycle', event => { hzs.push(event.target.hz) console.log(String(event.target)) }) .on('complete', function() { var fastest = this.filter('fastest')[0]; hzs = hzs.sort().reverse(); console.log(fastest.name, 'is', ((hzs[0] / hzs[1]) * 100).toFixed(2) + '% faster then the 2nd best'); console.log(fastest.name, 'is', ((hzs[0] / hzs[2]) * 100).toFixed(2) + '% faster then the 3rd best'); console.log() resolve(); }) .run({ 'async': true }) ; }); }; var obj = {x: 1, y: 2, z: 3}; obj.self = obj; var arr = ['x', 'y', 123, 'z']; arr.push(arr); Promise.resolve() .then(suite('global', global)) .then(suite('decycledGlobal', decycledGlobal)) .then(suite('empty object', {})) .then(suite('empty array', [])) .then(suite('small object', {x: 1, y: 2, z: 3})) .then(suite('self referencing small object', obj)) .then(suite('small array', ['x', 'y', 123, 'z'])) .then(suite('self referencing small array', arr)) .then(suite('string', 'this" is \' a test\t\n')) .then(suite('number', 1234)) .then(suite('null', null)) package/lib/cycle.js000644 0000015123 3560116604 011465 0ustar00000000 000000 var pathGetter = require('./path-getter'); var utils = require('./utils'); var WMap = typeof WeakMap !== 'undefined'? WeakMap: function() { var keys = []; var values = []; return { set: function(key, value) { keys.push(key); values.push(value); }, get: function(key) { for (var i = 0; i < keys.length; i++) { if (keys[i] === key) { return values[i]; } } } } }; // Based on https://github.com/douglascrockford/JSON-js/blob/master/cycle.js exports.decycle = function decycle(object, options, replacer, map) { 'use strict'; map = map || new WMap(); var noCircularOption = !Object.prototype.hasOwnProperty.call(options, 'circular'); var withRefs = options.refs !== false; return (function derez(_value, path, key) { // The derez recurses through the object, producing the deep copy. var i, // The loop counter name, // Property name nu; // The new object or array // typeof null === 'object', so go on if this value is really an object but not // one of the weird builtin objects. var value = typeof replacer === 'function' ? replacer(key || '', _value) : _value; if (options.date && value instanceof Date) { return {$jsan: 'd' + value.getTime()}; } if (options.regex && value instanceof RegExp) { return {$jsan: 'r' + utils.getRegexFlags(value) + ',' + value.source}; } if (options['function'] && typeof value === 'function') { return {$jsan: 'f' + utils.stringifyFunction(value, options['function'])} } if (options['nan'] && typeof value === 'number' && isNaN(value)) { return {$jsan: 'n'} } if (options['infinity']) { if (Number.POSITIVE_INFINITY === value) return {$jsan: 'i'} if (Number.NEGATIVE_INFINITY === value) return {$jsan: 'y'} } if (options['undefined'] && value === undefined) { return {$jsan: 'u'} } if (options['error'] && value instanceof Error) { return {$jsan: 'e' + value.message} } if (options['symbol'] && typeof value === 'symbol') { var symbolKey = Symbol.keyFor(value) if (symbolKey !== undefined) { return {$jsan: 'g' + symbolKey} } // 'Symbol(foo)'.slice(7, -1) === 'foo' return {$jsan: 's' + value.toString().slice(7, -1)} } if (options['map'] && typeof Map === 'function' && value instanceof Map && typeof Array.from === 'function') { return {$jsan: 'm' + JSON.stringify(decycle(Array.from(value), options, replacer, map))} } if (options['set'] && typeof Set === 'function' && value instanceof Set && typeof Array.from === 'function') { return {$jsan: 'l' + JSON.stringify(decycle(Array.from(value), options, replacer, map))} } if (value && typeof value.toJSON === 'function') { try { value = value.toJSON(key); } catch (error) { var keyString = (key || '$'); return "toJSON failed for '" + (map.get(value) || keyString) + "'"; } } if (typeof value === 'object' && value !== null && !(value instanceof Boolean) && !(value instanceof Date) && !(value instanceof Number) && !(value instanceof RegExp) && !(value instanceof String) && !(typeof value === 'symbol') && !(value instanceof Error)) { // If the value is an object or array, look to see if we have already // encountered it. If so, return a $ref/path object. if (typeof value === 'object') { var foundPath = map.get(value); if (foundPath) { if (noCircularOption && withRefs) { return {$jsan: foundPath}; } // This is only a true circular reference if the parent path is inside of foundPath // drop the last component of the current path and check if it starts with foundPath var parentPath = path.split('.').slice(0, -1).join('.'); if (parentPath.indexOf(foundPath) === 0) { if (!noCircularOption) { return typeof options.circular === 'function'? options.circular(value, path, foundPath): options.circular; } return {$jsan: foundPath}; } if (withRefs) return {$jsan: foundPath}; } map.set(value, path); } // If it is an array, replicate the array. if (Object.prototype.toString.apply(value) === '[object Array]') { nu = []; for (i = 0; i < value.length; i += 1) { nu[i] = derez(value[i], path + '[' + i + ']', i); } } else { // If it is an object, replicate the object. nu = {}; for (name in value) { if (Object.prototype.hasOwnProperty.call(value, name)) { var nextPath = /^\w+$/.test(name) ? '.' + name : '[' + JSON.stringify(name) + ']'; nu[name] = name === '$jsan' ? [derez(value[name], path + nextPath)] : derez(value[name], path + nextPath, name); } } } return nu; } return value; }(object, '$')); }; exports.retrocycle = function retrocycle($) { 'use strict'; return (function rez(value) { // The rez function walks recursively through the object looking for $jsan // properties. When it finds one that has a value that is a path, then it // replaces the $jsan object with a reference to the value that is found by // the path. var i, item, name, path; if (value && typeof value === 'object') { if (Object.prototype.toString.apply(value) === '[object Array]') { for (i = 0; i < value.length; i += 1) { item = value[i]; if (item && typeof item === 'object') { if (item.$jsan) { value[i] = utils.restore(item.$jsan, $); } else { rez(item); } } } } else { for (name in value) { // base case passed raw object if(typeof value[name] === 'string' && name === '$jsan'){ return utils.restore(value.$jsan, $); break; } else { if (name === '$jsan') { value[name] = value[name][0]; } if (typeof value[name] === 'object') { item = value[name]; if (item && typeof item === 'object') { if (item.$jsan) { value[name] = utils.restore(item.$jsan, $); } else { rez(item); } } } } } } } return value; }($)); }; package/test/end-to-end.js000644 0000007376 3560116604 012544 0ustar00000000 000000 var assert = require('assert'); var jsan = require('../'); var immutable = require('immutable'); describe('jsan', function() { it('can round trip a regular object', function() { var obj1 = {a: {b: {c: {d: 1}}}}; var obj2 = jsan.parse(jsan.stringify(obj1)); assert.deepEqual(obj1, obj2); }); it('can round trip a circular object', function() { var obj1 = {}; obj1['self'] = obj1; var obj2 = jsan.parse(jsan.stringify(obj1)); assert.deepEqual(obj2['self'], obj2); }); it('can round trip a self referencing objects', function() { var obj1 = {}; var subObj = {}; obj1.a = subObj; obj1.b = subObj; var obj2 = jsan.parse(jsan.stringify(obj1, null, null, true)); assert.deepEqual(obj2.a, obj2.b); }); it('can round trip dates', function() { var obj1 = { now: new Date() }; var obj2 = jsan.parse(jsan.stringify(obj1, null, null, true)); assert.deepEqual(obj1, obj2); }); it('can round trip regexs', function() { var obj1 = { r: /test/ }; var obj2 = jsan.parse(jsan.stringify(obj1, null, null, true)); assert.deepEqual(obj1, obj2); }); it('can round trip functions (toString())', function() { var obj1 = { f: function(foo) { bar } }; var obj2 = jsan.parse(jsan.stringify(obj1, null, null, true)); assert(obj2.f instanceof Function); assert.throws(obj2.f); }); it('can round trip undefined', function() { var obj1 = { u: undefined }; var obj2 = jsan.parse(jsan.stringify(obj1, null, null, true)); assert('u' in obj2 && obj2.u === undefined); }); it('can round trip errors', function() { var obj1 = { e: new Error('oh noh! :O') }; var obj2 = jsan.parse(jsan.stringify(obj1, null, null, true)); assert.deepEqual(obj1.e.message, obj2.e.message); }); it('can round trip a complex object', function() { var obj1 = { sub1: {}, now: new Date() }; obj1['self'] = obj1; obj1.sub2 = obj1.sub1; var obj2 = jsan.parse(jsan.stringify(obj1, null, null, true)); assert(obj2.now instanceof Date); assert.deepEqual(obj2.sub1, obj2.sub2); assert(obj2['self'] === obj2); }); it('allows a custom function toString()', function() { var obj1 = { f: function() { return 42; } }; var options = {}; options['function'] = function(fn) { return fn.toString().toUpperCase(); }; var obj2 = jsan.parse(jsan.stringify(obj1, null, null, options)); assert.deepEqual(obj2.f.toString(), obj1.f.toString().toUpperCase()); }); it("doesn't blow up for object with $jsan keys", function() { var obj1 = {$jsan: 'd1400000000000'}; var obj2 = jsan.parse(jsan.stringify(obj1, null, null, true)); assert.deepEqual(obj1, obj2); }); it("doesn't blow up for object with special $jsan keys", function() { var obj1 = {$jsan: new Date()}; var obj2 = jsan.parse(jsan.stringify(obj1, null, null, true)); assert.deepEqual(obj1, obj2); }); it("doesn't blow up on immutable.js", function() { var obj = { i: immutable.Map({ someList: immutable.List(), someMap: immutable.Map({ foo: function() {}, bar: 123 }) }) }; assert.deepEqual(JSON.stringify(obj), jsan.stringify(obj)); }); it("allows replacer functions when traversing", function() { var obj1 = { i: immutable.Map({ someList: immutable.List(), someMap: immutable.Map({ foo: function() {}, bar: 123 }) }) }; obj1.self = obj1; var obj2 = jsan.parse(jsan.stringify(obj1, function(key, value) { if (value && value.toJS) { return value.toJS(); } return value; }, null, true)); assert.deepEqual(obj2.i.someList, []); assert.deepEqual(obj2.self, obj2); assert(obj2.i.someMap.foo instanceof Function); }); }); package/index.js000644 0000000043 3560116604 010722 0ustar00000000 000000 module.exports = require('./lib'); package/lib/index.js000644 0000002371 3560116604 011476 0ustar00000000 000000 var cycle = require('./cycle'); exports.stringify = function stringify(value, replacer, space, _options) { if (arguments.length < 4) { try { if (arguments.length === 1) { return JSON.stringify(value); } else { return JSON.stringify.apply(JSON, arguments); } } catch (e) {} } var options = _options || false; if (typeof options === 'boolean') { options = { 'date': options, 'function': options, 'regex': options, 'undefined': options, 'error': options, 'symbol': options, 'map': options, 'set': options, 'nan': options, 'infinity': options } } var decycled = cycle.decycle(value, options, replacer); if (arguments.length === 1) { return JSON.stringify(decycled); } else { // decycle already handles when replacer is a function. return JSON.stringify(decycled, Array.isArray(replacer) ? replacer : null, space); } } exports.parse = function parse(text, reviver) { var needsRetrocycle = /"\$jsan"/.test(text); var parsed; if (arguments.length === 1) { parsed = JSON.parse(text); } else { parsed = JSON.parse(text, reviver); } if (needsRetrocycle) { parsed = cycle.retrocycle(parsed); } return parsed; } package/lib/path-getter.js000644 0000001135 3560116604 012610 0ustar00000000 000000 module.exports = pathGetter; function pathGetter(obj, path) { if (path !== '$') { var paths = getPaths(path); for (var i = 0; i < paths.length; i++) { path = paths[i].toString().replace(/\\"/g, '"'); if (typeof obj[path] === 'undefined' && i !== paths.length - 1) continue; obj = obj[path]; } } return obj; } function getPaths(pathString) { var regex = /(?:\.(\w+))|(?:\[(\d+)\])|(?:\["((?:[^\\"]|\\.)*)"\])/g; var matches = []; var match; while (match = regex.exec(pathString)) { matches.push( match[1] || match[2] || match[3] ); } return matches; } package/test/regression-cycle-map-set.js000644 0000000632 3560116604 015417 0ustar00000000 000000 var assert = require('assert'); var { stringify } = require('..'); describe('issues/32', function() { it('works', function() { const carrier = { id: "Carrier 1", map: new Map() }; const period = { id: "Period 1", carriers: new Set([carrier]) }; carrier.map.set(period, {}); const result = stringify([carrier], undefined, null, true); }); }); package/test/regression-function-newline.js000644 0000000520 3560116604 016234 0ustar00000000 000000 var assert = require('assert'); var jsan = require('../'); describe('newline before function curly brace', function() { it('still works', function() { var fn = function foo(a,b, c) { return 123; }; assert.equal(jsan.stringify(fn, null, null, true), '{"$jsan":"ffunction foo(a,b, c) { /* ... */ }"}'); }); }); package/test/regression-mobx.js000644 0000001306 3560116604 013720 0ustar00000000 000000 var assert = require('assert'); var jsan = require('../'); var mobx = require('mobx'); var todoFactory = function (title, store) { return mobx.observable({ store: store, // <-- remove this line to get it work title: title } ); }; var todoListFactory = function () { return mobx.observable({ todos: [], addTodo: mobx.action(function addTodo (todo) { this.todos.push(todo); }) }); }; describe('mobx case', function() { it('still works', function() { var store = todoListFactory(); store.addTodo(todoFactory('Write simpler code', store)); assert.equal(jsan.stringify(store), '{"todos":[{"store":{"$jsan":"$"},"title":"Write simpler code"}]}'); }); }); package/test/toJSON-error.js000644 0000001162 3560116604 013040 0ustar00000000 000000 var assert = require('assert'); var jsan = require('../'); describe('error in toJSON function', function() { it('still works', function() { var obj = { toJSON: function() { throw new Error('toJSON is unavailable'); } } assert.equal(jsan.stringify(obj, null, null, true), '"toJSON failed for \'$\'"'); assert.equal(jsan.stringify({ 'obj': obj }, null, null, true), '{"obj":"toJSON failed for \'obj\'"}'); var o = {}; o.self = o; o.noGood = obj; assert.equal(jsan.stringify(o, null, null, true), '{"self":{"$jsan":"$"},"noGood":"toJSON failed for \'noGood\'"}'); }); }); package/test/unit.js000644 0000036056 3560116604 011566 0ustar00000000 000000 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]); }); }); }); package/lib/utils.js000644 0000003365 3560116604 011533 0ustar00000000 000000 var pathGetter = require('./path-getter'); var jsan = require('./'); exports.getRegexFlags = function getRegexFlags(regex) { var flags = ''; if (regex.ignoreCase) flags += 'i'; if (regex.global) flags += 'g'; if (regex.multiline) flags += 'm'; return flags; }; exports.stringifyFunction = function stringifyFunction(fn, customToString) { if (typeof customToString === 'function') { return customToString(fn); } var str = fn.toString(); var match = str.match(/^[^{]*{|^[^=]*=>/); var start = match ? match[0] : ' '; var end = str[str.length - 1] === '}' ? '}' : ''; return start.replace(/\r\n|\n/g, ' ').replace(/\s+/g, ' ') + ' /* ... */ ' + end; }; exports.restore = function restore(obj, root) { var type = obj[0]; var rest = obj.slice(1); switch(type) { case '$': return pathGetter(root, obj); case 'r': var comma = rest.indexOf(','); var flags = rest.slice(0, comma); var source = rest.slice(comma + 1); return RegExp(source, flags); case 'd': return new Date(+rest); case 'f': var fn = function() { throw new Error("can't run jsan parsed function") }; fn.toString = function() { return rest; }; return fn; case 'u': return undefined; case 'e': var error = new Error(rest); error.stack = 'Stack is unavailable for jsan parsed errors'; return error; case 's': return Symbol(rest); case 'g': return Symbol.for(rest); case 'm': return new Map(jsan.parse(rest)); case 'l': return new Set(jsan.parse(rest)); case 'n': return NaN; case 'i': return Infinity; case 'y': return -Infinity; default: console.warn('unknown type', obj); return obj; } } package/package.json000644 0000001400 3560116604 011541 0ustar00000000 000000 { "name": "jsan", "version": "3.1.14", "description": "handle circular references when stringifying and parsing", "main": "index.js", "scripts": { "benchmark": "node perf/bench > perf/results.txt", "test": "mocha" }, "keywords": [ "json" ], "author": "Moshe Kolodny", "license": "MIT", "devDependencies": { "benchmark": "^2.1.2", "circular-json": "^0.3.0", "immutable": "^3.7.6", "json-stringify-safe": "^5.0.1", "mobx": "^2.4.1", "mocha": "^2.2.1", "rimraf": "^2.5.2" }, "dependencies": {}, "repository": { "type": "git", "url": "https://github.com/kolodny/jsan.git" }, "bugs": { "url": "https://github.com/kolodny/jsan/issues" }, "homepage": "https://github.com/kolodny/jsan" } package/perf/README.md000644 0000000417 3560116604 011475 0ustar00000000 000000 I ran some basic perf spot checks and it seems that `jsan` shines when stringifying small objects and arrays, and with large objects with no circular references. It does poorly compared to `CircularJSON` when stringifying the global object and ties the rest of the time. package/README.md000644 0000007275 3560116604 010552 0ustar00000000 000000 jsan === [![Build Status](https://travis-ci.org/kolodny/jsan.svg?branch=master)](https://travis-ci.org/kolodny/jsan) ### JavaScript "All The Things" Notation ![jsan](https://i.imgur.com/IdKDIB6.png) Easily stringify and parse any object including objects with circular references, self references, dates, regexes, `undefined`, errors, and even functions [1](#functions), using the familar `parse` and `stringify` methods. There are two ways to use this library, the first is to be able to serialize without having to worry about circular references, the second way is be able to handle dates, regexes, errors, functions [1](#functions), errors, and undefined (normally `JSON.stringify({ u: undefined }) === '{}'`) The usage reflect these two approaches. If you just want to be able to serialize an object then use `jsan.stringify(obj)`, if you want to JSON all the things then use it like `jsan.stringify(obj, null, null, true)`, the first three arguments are the same as `JSON.stringify` (yup, `JSON.stringify` takes three arguments) Note that `jsan.stringify(obj, null, null, true)` will also deal with circular references ### Usage ```js var jsan = require('jsan'); var obj = {}; obj['self'] = obj; obj['sub'] = {}; obj['sub']['subSelf'] = obj['sub']; obj.now = new Date(2015, 0, 1); var str = jsan.stringify(obj); str === '{"self":{"$jsan":"$"},"sub":{"subSelf":{"$jsan":"$.sub"}},"now":"2015-01-01T05:00:00.000Z"}'; // true var str2 = jsan.stringify(obj, null, null, true); str2 === '{"self":{"$jsan":"$"},"sub":{"subSelf":{"$jsan":"$.sub"}},"now":{"$jsan":"d1420088400000"}}'; // true var newObj1 = jsan.parse(str); newObj1 === newObj1['self']; // true newObj1['sub']['subSelf'] === newObj1['sub']; // true typeof newObj1.now === 'string'; // true var newObj2 = jsan.parse(str2); newObj2 === newObj2['self']; // true newObj2['sub']['subSelf'] === newObj2['sub']; // true newObj2.now instanceof Date; // true ``` #### Notes This ulitilty has been heavily optimized and performs as well as the native `JSON.parse` and `JSON.stringify`, for usages of `jsan.stringify(obj)` when there are no circular references. It does this by first `try { JSON.stringify(obj) }` and only when that fails, will it walk the object. Because of this it won't property handle self references that aren't circular by default. You can work around this by passing false as the fourth argument, or pass true and it will also handle dates, regexes, `undefeined`, errors, and functions ```js var obj = { r: /test/ }; var subObj = {}; obj.a = subObj; obj.b = subObj; var str1 = jsan.stringify(obj) // '{"r":{},a":{},"b":{}}' var str2 = jsan.stringify(obj, null, null, false) // '{"r":{},"a":{},"b":{"$jsan":"$.a"}}' var str3 = jsan.stringify(obj, null, null, true) // '{"r":{"$jsan":"r,test"},"a":{},"b":{"$jsan":"$.a"}}' ``` ##### Functions You can't execute the functions after `stringify()` and `parse()`, they are just functions that throw which have a `toString()` method similar to the original function ### Advance Usage You can specify how and what should be handled by passing an object as the fourth argument: ```js var obj = { u: undefined, r: /test/, f: function bar() {} }; var str = jsan.stringify(obj, null, null, { undefined: true, function: true }); // '{"u":{"$jsan":"u"},"r":{},"f":{"$jsan":"ffunction bar() { /* ... */ }"}}' ``` The `function` property of options can also take a function which will be used as the function stringifyer: ```js var obj = { u: undefined, r: /test/, f: function(x) { return x + 1 } }; var str = jsan.stringify(obj, null, null, { undefined: true, function: function(fn) { return fn.toString() } }); str === '{"u":{"$jsan":"u"},"r":{},"f":{"$jsan":"ffunction (x) { return x + 1 }"}}'; // true ``` package/perf/results.txt000644 0000005665 3560116604 012472 0ustar00000000 000000 global jsan x 780 ops/sec ±1.44% (80 runs sampled) CircularJSON x 1,697 ops/sec ±1.45% (83 runs sampled) json-stringify-safe x 1,598 ops/sec ±1.02% (82 runs sampled) CircularJSON is 45.96% faster then the 2nd best CircularJSON is 48.82% faster then the 3rd best decycledGlobal jsan x 7,749 ops/sec ±1.05% (86 runs sampled) CircularJSON x 2,366 ops/sec ±1.18% (83 runs sampled) json-stringify-safe x 2,452 ops/sec ±1.47% (83 runs sampled) jsan is 315.99% faster then the 2nd best jsan is 327.48% faster then the 3rd best empty object jsan x 2,668,800 ops/sec ±1.18% (85 runs sampled) CircularJSON x 1,330,493 ops/sec ±1.36% (84 runs sampled) json-stringify-safe x 1,111,661 ops/sec ±1.19% (87 runs sampled) jsan is 200.59% faster then the 2nd best jsan is 240.07% faster then the 3rd best empty array jsan x 2,621,901 ops/sec ±1.13% (87 runs sampled) CircularJSON x 1,314,142 ops/sec ±1.44% (88 runs sampled) json-stringify-safe x 1,111,390 ops/sec ±1.22% (84 runs sampled) jsan is 199.51% faster then the 2nd best jsan is 235.91% faster then the 3rd best small object jsan x 1,797,778 ops/sec ±1.22% (81 runs sampled) CircularJSON x 822,628 ops/sec ±1.06% (83 runs sampled) json-stringify-safe x 502,463 ops/sec ±1.57% (85 runs sampled) jsan is 163.72% faster then the 2nd best jsan is 45.76% faster then the 3rd best self referencing small object jsan x 100,878 ops/sec ±1.42% (85 runs sampled) CircularJSON x 649,786 ops/sec ±1.52% (82 runs sampled) json-stringify-safe x 399,026 ops/sec ±1.00% (88 runs sampled) CircularJSON is 162.84% faster then the 2nd best CircularJSON is 644.13% faster then the 3rd best small array jsan x 2,009,982 ops/sec ±1.07% (86 runs sampled) CircularJSON x 469,268 ops/sec ±1.25% (87 runs sampled) json-stringify-safe x 416,464 ops/sec ±1.43% (86 runs sampled) jsan is 112.68% faster then the 2nd best jsan is 23.35% faster then the 3rd best self referencing small array jsan x 111,812 ops/sec ±1.33% (84 runs sampled) CircularJSON x 423,330 ops/sec ±1.18% (85 runs sampled) json-stringify-safe x 345,112 ops/sec ±1.16% (87 runs sampled) CircularJSON is 122.66% faster then the 2nd best CircularJSON is 378.61% faster then the 3rd best string jsan x 3,170,617 ops/sec ±1.22% (85 runs sampled) CircularJSON x 1,521,238 ops/sec ±1.36% (85 runs sampled) json-stringify-safe x 1,192,485 ops/sec ±1.09% (84 runs sampled) jsan is 208.42% faster then the 2nd best jsan is 265.88% faster then the 3rd best number jsan x 3,783,106 ops/sec ±1.03% (85 runs sampled) CircularJSON x 1,618,607 ops/sec ±1.26% (84 runs sampled) json-stringify-safe x 1,285,919 ops/sec ±1.32% (86 runs sampled) jsan is 233.73% faster then the 2nd best jsan is 294.19% faster then the 3rd best null jsan x 3,861,908 ops/sec ±1.43% (81 runs sampled) CircularJSON x 1,710,725 ops/sec ±1.21% (85 runs sampled) json-stringify-safe x 1,390,944 ops/sec ±1.04% (85 runs sampled) jsan is 225.75% faster then the 2nd best jsan is 277.65% faster then the 3rd best package/.travis.yml000644 0000000074 3560116604 011372 0ustar00000000 000000 language: node_js node_js: - '6' - '8' - '9' - '10'