├── .gitignore ├── index.js ├── .travis.yml ├── .editorconfig ├── perf ├── README.md ├── bench.js └── results.txt ├── test ├── regression-function-newline.js ├── regression-cycle-map-set.js ├── toJSON-error.js ├── regression-mobx.js ├── end-to-end.js └── unit.js ├── lib ├── path-getter.js ├── index.js ├── utils.js └── cycle.js ├── package.json ├── LICENSE └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./lib'); 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '6' 4 | - '8' 5 | - '9' 6 | - '10' 7 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /perf/README.md: -------------------------------------------------------------------------------- 1 | I ran some basic perf spot checks and it seems that `jsan` shines when stringifying 2 | small objects and arrays, and with large objects with no circular references. It does 3 | poorly compared to `CircularJSON` when stringifying the global object and ties 4 | the rest of the time. 5 | -------------------------------------------------------------------------------- /test/regression-function-newline.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | var jsan = require('../'); 3 | 4 | describe('newline before function curly brace', function() { 5 | it('still works', function() { 6 | var fn = function foo(a,b, c) 7 | { 8 | return 123; 9 | }; 10 | assert.equal(jsan.stringify(fn, null, null, true), '{"$jsan":"ffunction foo(a,b, c) { /* ... */ }"}'); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /test/regression-cycle-map-set.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | var { stringify } = require('..'); 3 | 4 | describe('issues/32', function() { 5 | it('works', function() { 6 | const carrier = { 7 | id: "Carrier 1", 8 | map: new Map() 9 | }; 10 | 11 | const period = { 12 | id: "Period 1", 13 | carriers: new Set([carrier]) 14 | }; 15 | 16 | carrier.map.set(period, {}); 17 | 18 | const result = stringify([carrier], undefined, null, true); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /test/toJSON-error.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | var jsan = require('../'); 3 | 4 | describe('error in toJSON function', function() { 5 | it('still works', function() { 6 | var obj = { 7 | toJSON: function() { 8 | throw new Error('toJSON is unavailable'); 9 | } 10 | } 11 | assert.equal(jsan.stringify(obj, null, null, true), '"toJSON failed for \'$\'"'); 12 | assert.equal(jsan.stringify({ 'obj': obj }, null, null, true), '{"obj":"toJSON failed for \'obj\'"}'); 13 | var o = {}; 14 | o.self = o; 15 | o.noGood = obj; 16 | assert.equal(jsan.stringify(o, null, null, true), '{"self":{"$jsan":"$"},"noGood":"toJSON failed for \'noGood\'"}'); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /lib/path-getter.js: -------------------------------------------------------------------------------- 1 | module.exports = pathGetter; 2 | 3 | function pathGetter(obj, path) { 4 | if (path !== '$') { 5 | var paths = getPaths(path); 6 | for (var i = 0; i < paths.length; i++) { 7 | path = paths[i].toString().replace(/\\"/g, '"'); 8 | if (typeof obj[path] === 'undefined' && i !== paths.length - 1) continue; 9 | obj = obj[path]; 10 | } 11 | } 12 | return obj; 13 | } 14 | 15 | function getPaths(pathString) { 16 | var regex = /(?:\.(\w+))|(?:\[(\d+)\])|(?:\["((?:[^\\"]|\\.)*)"\])/g; 17 | var matches = []; 18 | var match; 19 | while (match = regex.exec(pathString)) { 20 | matches.push( match[1] || match[2] || match[3] ); 21 | } 22 | return matches; 23 | } 24 | -------------------------------------------------------------------------------- /test/regression-mobx.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | var jsan = require('../'); 3 | var mobx = require('mobx'); 4 | 5 | var todoFactory = function (title, store) { 6 | return mobx.observable({ 7 | store: store, // <-- remove this line to get it work 8 | title: title 9 | } 10 | ); 11 | }; 12 | 13 | var todoListFactory = function () { 14 | return mobx.observable({ 15 | todos: [], 16 | addTodo: mobx.action(function addTodo (todo) { 17 | this.todos.push(todo); 18 | }) 19 | }); 20 | }; 21 | 22 | describe('mobx case', function() { 23 | it('still works', function() { 24 | var store = todoListFactory(); 25 | store.addTodo(todoFactory('Write simpler code', store)); 26 | assert.equal(jsan.stringify(store), '{"todos":[{"store":{"$jsan":"$"},"title":"Write simpler code"}]}'); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jsan", 3 | "version": "3.1.14", 4 | "description": "handle circular references when stringifying and parsing", 5 | "main": "index.js", 6 | "scripts": { 7 | "benchmark": "node perf/bench > perf/results.txt", 8 | "test": "mocha" 9 | }, 10 | "keywords": [ 11 | "json" 12 | ], 13 | "author": "Moshe Kolodny", 14 | "license": "MIT", 15 | "devDependencies": { 16 | "benchmark": "^2.1.2", 17 | "circular-json": "^0.3.0", 18 | "immutable": "^3.7.6", 19 | "json-stringify-safe": "^5.0.1", 20 | "mobx": "^2.4.1", 21 | "mocha": "^2.2.1", 22 | "rimraf": "^2.5.2" 23 | }, 24 | "dependencies": {}, 25 | "repository": { 26 | "type": "git", 27 | "url": "https://github.com/kolodny/jsan.git" 28 | }, 29 | "bugs": { 30 | "url": "https://github.com/kolodny/jsan/issues" 31 | }, 32 | "homepage": "https://github.com/kolodny/jsan" 33 | } 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018 Moshe Kolodny 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | var cycle = require('./cycle'); 2 | 3 | exports.stringify = function stringify(value, replacer, space, _options) { 4 | 5 | if (arguments.length < 4) { 6 | try { 7 | if (arguments.length === 1) { 8 | return JSON.stringify(value); 9 | } else { 10 | return JSON.stringify.apply(JSON, arguments); 11 | } 12 | } catch (e) {} 13 | } 14 | 15 | var options = _options || false; 16 | if (typeof options === 'boolean') { 17 | options = { 18 | 'date': options, 19 | 'function': options, 20 | 'regex': options, 21 | 'undefined': options, 22 | 'error': options, 23 | 'symbol': options, 24 | 'map': options, 25 | 'set': options, 26 | 'nan': options, 27 | 'infinity': options 28 | } 29 | } 30 | 31 | var decycled = cycle.decycle(value, options, replacer); 32 | if (arguments.length === 1) { 33 | return JSON.stringify(decycled); 34 | } else { 35 | // decycle already handles when replacer is a function. 36 | return JSON.stringify(decycled, Array.isArray(replacer) ? replacer : null, space); 37 | } 38 | } 39 | 40 | exports.parse = function parse(text, reviver) { 41 | var needsRetrocycle = /"\$jsan"/.test(text); 42 | var parsed; 43 | if (arguments.length === 1) { 44 | parsed = JSON.parse(text); 45 | } else { 46 | parsed = JSON.parse(text, reviver); 47 | } 48 | if (needsRetrocycle) { 49 | parsed = cycle.retrocycle(parsed); 50 | } 51 | return parsed; 52 | } 53 | -------------------------------------------------------------------------------- /perf/bench.js: -------------------------------------------------------------------------------- 1 | var jsan = require('../'); 2 | var decycle = require('../lib/cycle').decycle; 3 | 4 | var CircularJSON = require('circular-json'); 5 | var stringify = require('json-stringify-safe'); 6 | 7 | var Benchmark = require('benchmark'); 8 | 9 | var decycledGlobal = decycle(global, {}); 10 | 11 | const suite = (name, obj) => () => { 12 | return new Promise(function(resolve) { 13 | var hzs = [] 14 | console.log(name) 15 | new Benchmark.Suite(name) 16 | .add('jsan', () => jsan.stringify(obj)) 17 | .add('CircularJSON', () => CircularJSON.stringify(obj)) 18 | .add('json-stringify-safe', () => stringify(obj)) 19 | .on('cycle', event => { 20 | hzs.push(event.target.hz) 21 | console.log(String(event.target)) 22 | }) 23 | .on('complete', function() { 24 | var fastest = this.filter('fastest')[0]; 25 | hzs = hzs.sort().reverse(); 26 | console.log(fastest.name, 'is', ((hzs[0] / hzs[1]) * 100).toFixed(2) + '% faster then the 2nd best'); 27 | console.log(fastest.name, 'is', ((hzs[0] / hzs[2]) * 100).toFixed(2) + '% faster then the 3rd best'); 28 | console.log() 29 | resolve(); 30 | }) 31 | .run({ 'async': true }) 32 | ; 33 | }); 34 | }; 35 | 36 | var obj = {x: 1, y: 2, z: 3}; 37 | obj.self = obj; 38 | 39 | var arr = ['x', 'y', 123, 'z']; 40 | arr.push(arr); 41 | 42 | 43 | Promise.resolve() 44 | .then(suite('global', global)) 45 | .then(suite('decycledGlobal', decycledGlobal)) 46 | .then(suite('empty object', {})) 47 | .then(suite('empty array', [])) 48 | 49 | .then(suite('small object', {x: 1, y: 2, z: 3})) 50 | .then(suite('self referencing small object', obj)) 51 | 52 | .then(suite('small array', ['x', 'y', 123, 'z'])) 53 | .then(suite('self referencing small array', arr)) 54 | 55 | .then(suite('string', 'this" is \' a test\t\n')) 56 | .then(suite('number', 1234)) 57 | .then(suite('null', null)) 58 | -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | var pathGetter = require('./path-getter'); 2 | var jsan = require('./'); 3 | 4 | exports.getRegexFlags = function getRegexFlags(regex) { 5 | var flags = ''; 6 | if (regex.ignoreCase) flags += 'i'; 7 | if (regex.global) flags += 'g'; 8 | if (regex.multiline) flags += 'm'; 9 | return flags; 10 | }; 11 | 12 | exports.stringifyFunction = function stringifyFunction(fn, customToString) { 13 | if (typeof customToString === 'function') { 14 | return customToString(fn); 15 | } 16 | var str = fn.toString(); 17 | var match = str.match(/^[^{]*{|^[^=]*=>/); 18 | var start = match ? match[0] : ' '; 19 | var end = str[str.length - 1] === '}' ? '}' : ''; 20 | return start.replace(/\r\n|\n/g, ' ').replace(/\s+/g, ' ') + ' /* ... */ ' + end; 21 | }; 22 | 23 | exports.restore = function restore(obj, root) { 24 | var type = obj[0]; 25 | var rest = obj.slice(1); 26 | switch(type) { 27 | case '$': 28 | return pathGetter(root, obj); 29 | case 'r': 30 | var comma = rest.indexOf(','); 31 | var flags = rest.slice(0, comma); 32 | var source = rest.slice(comma + 1); 33 | return RegExp(source, flags); 34 | case 'd': 35 | return new Date(+rest); 36 | case 'f': 37 | var fn = function() { throw new Error("can't run jsan parsed function") }; 38 | fn.toString = function() { return rest; }; 39 | return fn; 40 | case 'u': 41 | return undefined; 42 | case 'e': 43 | var error = new Error(rest); 44 | error.stack = 'Stack is unavailable for jsan parsed errors'; 45 | return error; 46 | case 's': 47 | return Symbol(rest); 48 | case 'g': 49 | return Symbol.for(rest); 50 | case 'm': 51 | return new Map(jsan.parse(rest)); 52 | case 'l': 53 | return new Set(jsan.parse(rest)); 54 | case 'n': 55 | return NaN; 56 | case 'i': 57 | return Infinity; 58 | case 'y': 59 | return -Infinity; 60 | default: 61 | console.warn('unknown type', obj); 62 | return obj; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /perf/results.txt: -------------------------------------------------------------------------------- 1 | global 2 | jsan x 780 ops/sec ±1.44% (80 runs sampled) 3 | CircularJSON x 1,697 ops/sec ±1.45% (83 runs sampled) 4 | json-stringify-safe x 1,598 ops/sec ±1.02% (82 runs sampled) 5 | CircularJSON is 45.96% faster then the 2nd best 6 | CircularJSON is 48.82% faster then the 3rd best 7 | 8 | decycledGlobal 9 | jsan x 7,749 ops/sec ±1.05% (86 runs sampled) 10 | CircularJSON x 2,366 ops/sec ±1.18% (83 runs sampled) 11 | json-stringify-safe x 2,452 ops/sec ±1.47% (83 runs sampled) 12 | jsan is 315.99% faster then the 2nd best 13 | jsan is 327.48% faster then the 3rd best 14 | 15 | empty object 16 | jsan x 2,668,800 ops/sec ±1.18% (85 runs sampled) 17 | CircularJSON x 1,330,493 ops/sec ±1.36% (84 runs sampled) 18 | json-stringify-safe x 1,111,661 ops/sec ±1.19% (87 runs sampled) 19 | jsan is 200.59% faster then the 2nd best 20 | jsan is 240.07% faster then the 3rd best 21 | 22 | empty array 23 | jsan x 2,621,901 ops/sec ±1.13% (87 runs sampled) 24 | CircularJSON x 1,314,142 ops/sec ±1.44% (88 runs sampled) 25 | json-stringify-safe x 1,111,390 ops/sec ±1.22% (84 runs sampled) 26 | jsan is 199.51% faster then the 2nd best 27 | jsan is 235.91% faster then the 3rd best 28 | 29 | small object 30 | jsan x 1,797,778 ops/sec ±1.22% (81 runs sampled) 31 | CircularJSON x 822,628 ops/sec ±1.06% (83 runs sampled) 32 | json-stringify-safe x 502,463 ops/sec ±1.57% (85 runs sampled) 33 | jsan is 163.72% faster then the 2nd best 34 | jsan is 45.76% faster then the 3rd best 35 | 36 | self referencing small object 37 | jsan x 100,878 ops/sec ±1.42% (85 runs sampled) 38 | CircularJSON x 649,786 ops/sec ±1.52% (82 runs sampled) 39 | json-stringify-safe x 399,026 ops/sec ±1.00% (88 runs sampled) 40 | CircularJSON is 162.84% faster then the 2nd best 41 | CircularJSON is 644.13% faster then the 3rd best 42 | 43 | small array 44 | jsan x 2,009,982 ops/sec ±1.07% (86 runs sampled) 45 | CircularJSON x 469,268 ops/sec ±1.25% (87 runs sampled) 46 | json-stringify-safe x 416,464 ops/sec ±1.43% (86 runs sampled) 47 | jsan is 112.68% faster then the 2nd best 48 | jsan is 23.35% faster then the 3rd best 49 | 50 | self referencing small array 51 | jsan x 111,812 ops/sec ±1.33% (84 runs sampled) 52 | CircularJSON x 423,330 ops/sec ±1.18% (85 runs sampled) 53 | json-stringify-safe x 345,112 ops/sec ±1.16% (87 runs sampled) 54 | CircularJSON is 122.66% faster then the 2nd best 55 | CircularJSON is 378.61% faster then the 3rd best 56 | 57 | string 58 | jsan x 3,170,617 ops/sec ±1.22% (85 runs sampled) 59 | CircularJSON x 1,521,238 ops/sec ±1.36% (85 runs sampled) 60 | json-stringify-safe x 1,192,485 ops/sec ±1.09% (84 runs sampled) 61 | jsan is 208.42% faster then the 2nd best 62 | jsan is 265.88% faster then the 3rd best 63 | 64 | number 65 | jsan x 3,783,106 ops/sec ±1.03% (85 runs sampled) 66 | CircularJSON x 1,618,607 ops/sec ±1.26% (84 runs sampled) 67 | json-stringify-safe x 1,285,919 ops/sec ±1.32% (86 runs sampled) 68 | jsan is 233.73% faster then the 2nd best 69 | jsan is 294.19% faster then the 3rd best 70 | 71 | null 72 | jsan x 3,861,908 ops/sec ±1.43% (81 runs sampled) 73 | CircularJSON x 1,710,725 ops/sec ±1.21% (85 runs sampled) 74 | json-stringify-safe x 1,390,944 ops/sec ±1.04% (85 runs sampled) 75 | jsan is 225.75% faster then the 2nd best 76 | jsan is 277.65% faster then the 3rd best 77 | 78 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | jsan 2 | === 3 | 4 | [![Build Status](https://travis-ci.org/kolodny/jsan.svg?branch=master)](https://travis-ci.org/kolodny/jsan) 5 | 6 | ### JavaScript "All The Things" Notation 7 | ![jsan](https://i.imgur.com/IdKDIB6.png) 8 | 9 | Easily stringify and parse any object including objects with circular references, self references, dates, regexes, `undefined`, errors, and even functions 10 | [1](#functions), using the familar `parse` and `stringify` methods. 11 | 12 | There are two ways to use this library, the first is to be able to 13 | serialize without having to worry about circular references, 14 | the second way is be able to handle dates, regexes, errors, functions 15 | [1](#functions), errors, and undefined (normally 16 | `JSON.stringify({ u: undefined }) === '{}'`) 17 | 18 | The usage reflect these two approaches. If you just want to be 19 | able to serialize an object then use `jsan.stringify(obj)`, 20 | if you want to JSON all the things then use it like 21 | `jsan.stringify(obj, null, null, true)`, the first three 22 | arguments are the same as `JSON.stringify` (yup, `JSON.stringify` 23 | takes three arguments) 24 | 25 | Note that `jsan.stringify(obj, null, null, true)` will also deal 26 | with circular references 27 | 28 | 29 | ### Usage 30 | 31 | ```js 32 | var jsan = require('jsan'); 33 | 34 | var obj = {}; 35 | obj['self'] = obj; 36 | obj['sub'] = {}; 37 | obj['sub']['subSelf'] = obj['sub']; 38 | obj.now = new Date(2015, 0, 1); 39 | 40 | var str = jsan.stringify(obj); 41 | str === '{"self":{"$jsan":"$"},"sub":{"subSelf":{"$jsan":"$.sub"}},"now":"2015-01-01T05:00:00.000Z"}'; // true 42 | var str2 = jsan.stringify(obj, null, null, true); 43 | str2 === '{"self":{"$jsan":"$"},"sub":{"subSelf":{"$jsan":"$.sub"}},"now":{"$jsan":"d1420088400000"}}'; // true 44 | 45 | var newObj1 = jsan.parse(str); 46 | newObj1 === newObj1['self']; // true 47 | newObj1['sub']['subSelf'] === newObj1['sub']; // true 48 | typeof newObj1.now === 'string'; // true 49 | 50 | var newObj2 = jsan.parse(str2); 51 | newObj2 === newObj2['self']; // true 52 | newObj2['sub']['subSelf'] === newObj2['sub']; // true 53 | newObj2.now instanceof Date; // true 54 | ``` 55 | 56 | #### Notes 57 | 58 | This ulitilty has been heavily optimized and performs as well as the native `JSON.parse` and 59 | `JSON.stringify`, for usages of `jsan.stringify(obj)` when there are no circular references. 60 | It does this by first `try { JSON.stringify(obj) }` and only when that fails, will it walk 61 | the object. Because of this it won't property handle self references that aren't circular by 62 | default. You can work around this by passing false as the fourth argument, or pass true and it 63 | will also handle dates, regexes, `undefeined`, errors, and functions 64 | 65 | ```js 66 | var obj = { r: /test/ }; 67 | var subObj = {}; 68 | obj.a = subObj; 69 | obj.b = subObj; 70 | var str1 = jsan.stringify(obj) // '{"r":{},a":{},"b":{}}' 71 | var str2 = jsan.stringify(obj, null, null, false) // '{"r":{},"a":{},"b":{"$jsan":"$.a"}}' 72 | var str3 = jsan.stringify(obj, null, null, true) // '{"r":{"$jsan":"r,test"},"a":{},"b":{"$jsan":"$.a"}}' 73 | ``` 74 | 75 | ##### Functions 76 | 77 | You can't execute the functions after `stringify()` and `parse()`, they are just functions 78 | that throw which have a `toString()` method similar to the original function 79 | 80 | ### Advance Usage 81 | 82 | You can specify how and what should be handled by passing an object as the fourth argument: 83 | 84 | ```js 85 | var obj = { u: undefined, r: /test/, f: function bar() {} }; 86 | var str = jsan.stringify(obj, null, null, { undefined: true, function: true }); // '{"u":{"$jsan":"u"},"r":{},"f":{"$jsan":"ffunction bar() { /* ... */ }"}}' 87 | ``` 88 | 89 | The `function` property of options can also take a function which will be used as the 90 | function stringifyer: 91 | 92 | ```js 93 | var obj = { u: undefined, r: /test/, f: function(x) { return x + 1 } }; 94 | var str = jsan.stringify(obj, null, null, { 95 | undefined: true, 96 | function: function(fn) { return fn.toString() } 97 | }); 98 | str === '{"u":{"$jsan":"u"},"r":{},"f":{"$jsan":"ffunction (x) { return x + 1 }"}}'; // true 99 | ``` 100 | -------------------------------------------------------------------------------- /test/end-to-end.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | var jsan = require('../'); 3 | var immutable = require('immutable'); 4 | 5 | describe('jsan', function() { 6 | 7 | 8 | it('can round trip a regular object', function() { 9 | var obj1 = {a: {b: {c: {d: 1}}}}; 10 | var obj2 = jsan.parse(jsan.stringify(obj1)); 11 | assert.deepEqual(obj1, obj2); 12 | }); 13 | 14 | it('can round trip a circular object', function() { 15 | var obj1 = {}; 16 | obj1['self'] = obj1; 17 | var obj2 = jsan.parse(jsan.stringify(obj1)); 18 | assert.deepEqual(obj2['self'], obj2); 19 | }); 20 | 21 | it('can round trip a self referencing objects', function() { 22 | var obj1 = {}; 23 | var subObj = {}; 24 | obj1.a = subObj; 25 | obj1.b = subObj; 26 | var obj2 = jsan.parse(jsan.stringify(obj1, null, null, true)); 27 | assert.deepEqual(obj2.a, obj2.b); 28 | }); 29 | 30 | it('can round trip dates', function() { 31 | var obj1 = { now: new Date() }; 32 | var obj2 = jsan.parse(jsan.stringify(obj1, null, null, true)); 33 | assert.deepEqual(obj1, obj2); 34 | }); 35 | 36 | it('can round trip regexs', function() { 37 | var obj1 = { r: /test/ }; 38 | var obj2 = jsan.parse(jsan.stringify(obj1, null, null, true)); 39 | assert.deepEqual(obj1, obj2); 40 | }); 41 | 42 | it('can round trip functions (toString())', function() { 43 | var obj1 = { f: function(foo) { bar } }; 44 | var obj2 = jsan.parse(jsan.stringify(obj1, null, null, true)); 45 | assert(obj2.f instanceof Function); 46 | assert.throws(obj2.f); 47 | }); 48 | 49 | it('can round trip undefined', function() { 50 | var obj1 = { u: undefined }; 51 | var obj2 = jsan.parse(jsan.stringify(obj1, null, null, true)); 52 | assert('u' in obj2 && obj2.u === undefined); 53 | }); 54 | 55 | it('can round trip errors', function() { 56 | var obj1 = { e: new Error('oh noh! :O') }; 57 | var obj2 = jsan.parse(jsan.stringify(obj1, null, null, true)); 58 | assert.deepEqual(obj1.e.message, obj2.e.message); 59 | }); 60 | 61 | it('can round trip a complex object', function() { 62 | var obj1 = { 63 | sub1: {}, 64 | now: new Date() 65 | }; 66 | obj1['self'] = obj1; 67 | obj1.sub2 = obj1.sub1; 68 | var obj2 = jsan.parse(jsan.stringify(obj1, null, null, true)); 69 | assert(obj2.now instanceof Date); 70 | assert.deepEqual(obj2.sub1, obj2.sub2); 71 | assert(obj2['self'] === obj2); 72 | }); 73 | 74 | it('allows a custom function toString()', function() { 75 | var obj1 = { f: function() { return 42; } }; 76 | var options = {}; 77 | options['function'] = function(fn) { return fn.toString().toUpperCase(); }; 78 | var obj2 = jsan.parse(jsan.stringify(obj1, null, null, options)); 79 | assert.deepEqual(obj2.f.toString(), obj1.f.toString().toUpperCase()); 80 | }); 81 | 82 | it("doesn't blow up for object with $jsan keys", function() { 83 | var obj1 = {$jsan: 'd1400000000000'}; 84 | var obj2 = jsan.parse(jsan.stringify(obj1, null, null, true)); 85 | assert.deepEqual(obj1, obj2); 86 | }); 87 | 88 | it("doesn't blow up for object with special $jsan keys", function() { 89 | var obj1 = {$jsan: new Date()}; 90 | var obj2 = jsan.parse(jsan.stringify(obj1, null, null, true)); 91 | assert.deepEqual(obj1, obj2); 92 | }); 93 | 94 | it("doesn't blow up on immutable.js", function() { 95 | var obj = { 96 | i: immutable.Map({ 97 | someList: immutable.List(), 98 | someMap: immutable.Map({ 99 | foo: function() {}, 100 | bar: 123 101 | }) 102 | }) 103 | }; 104 | assert.deepEqual(JSON.stringify(obj), jsan.stringify(obj)); 105 | }); 106 | 107 | it("allows replacer functions when traversing", function() { 108 | var obj1 = { 109 | i: immutable.Map({ 110 | someList: immutable.List(), 111 | someMap: immutable.Map({ 112 | foo: function() {}, 113 | bar: 123 114 | }) 115 | }) 116 | }; 117 | obj1.self = obj1; 118 | var obj2 = jsan.parse(jsan.stringify(obj1, function(key, value) { 119 | if (value && value.toJS) { return value.toJS(); } 120 | return value; 121 | }, null, true)); 122 | assert.deepEqual(obj2.i.someList, []); 123 | assert.deepEqual(obj2.self, obj2); 124 | assert(obj2.i.someMap.foo instanceof Function); 125 | }); 126 | 127 | 128 | }); 129 | -------------------------------------------------------------------------------- /lib/cycle.js: -------------------------------------------------------------------------------- 1 | var pathGetter = require('./path-getter'); 2 | var utils = require('./utils'); 3 | 4 | var WMap = typeof WeakMap !== 'undefined'? 5 | WeakMap: 6 | function() { 7 | var keys = []; 8 | var values = []; 9 | return { 10 | set: function(key, value) { 11 | keys.push(key); 12 | values.push(value); 13 | }, 14 | get: function(key) { 15 | for (var i = 0; i < keys.length; i++) { 16 | if (keys[i] === key) { 17 | return values[i]; 18 | } 19 | } 20 | } 21 | } 22 | }; 23 | 24 | // Based on https://github.com/douglascrockford/JSON-js/blob/master/cycle.js 25 | 26 | exports.decycle = function decycle(object, options, replacer, map) { 27 | 'use strict'; 28 | 29 | map = map || new WMap(); 30 | 31 | var noCircularOption = !Object.prototype.hasOwnProperty.call(options, 'circular'); 32 | var withRefs = options.refs !== false; 33 | 34 | return (function derez(_value, path, key) { 35 | 36 | // The derez recurses through the object, producing the deep copy. 37 | 38 | var i, // The loop counter 39 | name, // Property name 40 | nu; // The new object or array 41 | 42 | // typeof null === 'object', so go on if this value is really an object but not 43 | // one of the weird builtin objects. 44 | 45 | var value = typeof replacer === 'function' ? replacer(key || '', _value) : _value; 46 | 47 | if (options.date && value instanceof Date) { 48 | return {$jsan: 'd' + value.getTime()}; 49 | } 50 | if (options.regex && value instanceof RegExp) { 51 | return {$jsan: 'r' + utils.getRegexFlags(value) + ',' + value.source}; 52 | } 53 | if (options['function'] && typeof value === 'function') { 54 | return {$jsan: 'f' + utils.stringifyFunction(value, options['function'])} 55 | } 56 | if (options['nan'] && typeof value === 'number' && isNaN(value)) { 57 | return {$jsan: 'n'} 58 | } 59 | if (options['infinity']) { 60 | if (Number.POSITIVE_INFINITY === value) return {$jsan: 'i'} 61 | if (Number.NEGATIVE_INFINITY === value) return {$jsan: 'y'} 62 | } 63 | if (options['undefined'] && value === undefined) { 64 | return {$jsan: 'u'} 65 | } 66 | if (options['error'] && value instanceof Error) { 67 | return {$jsan: 'e' + value.message} 68 | } 69 | if (options['symbol'] && typeof value === 'symbol') { 70 | var symbolKey = Symbol.keyFor(value) 71 | if (symbolKey !== undefined) { 72 | return {$jsan: 'g' + symbolKey} 73 | } 74 | 75 | // 'Symbol(foo)'.slice(7, -1) === 'foo' 76 | return {$jsan: 's' + value.toString().slice(7, -1)} 77 | } 78 | 79 | if (options['map'] && typeof Map === 'function' && value instanceof Map && typeof Array.from === 'function') { 80 | return {$jsan: 'm' + JSON.stringify(decycle(Array.from(value), options, replacer, map))} 81 | } 82 | 83 | if (options['set'] && typeof Set === 'function' && value instanceof Set && typeof Array.from === 'function') { 84 | return {$jsan: 'l' + JSON.stringify(decycle(Array.from(value), options, replacer, map))} 85 | } 86 | 87 | if (value && typeof value.toJSON === 'function') { 88 | try { 89 | value = value.toJSON(key); 90 | } catch (error) { 91 | var keyString = (key || '$'); 92 | return "toJSON failed for '" + (map.get(value) || keyString) + "'"; 93 | } 94 | } 95 | 96 | if (typeof value === 'object' && value !== null && 97 | !(value instanceof Boolean) && 98 | !(value instanceof Date) && 99 | !(value instanceof Number) && 100 | !(value instanceof RegExp) && 101 | !(value instanceof String) && 102 | !(typeof value === 'symbol') && 103 | !(value instanceof Error)) { 104 | 105 | // If the value is an object or array, look to see if we have already 106 | // encountered it. If so, return a $ref/path object. 107 | 108 | if (typeof value === 'object') { 109 | var foundPath = map.get(value); 110 | if (foundPath) { 111 | if (noCircularOption && withRefs) { 112 | return {$jsan: foundPath}; 113 | } 114 | 115 | // This is only a true circular reference if the parent path is inside of foundPath 116 | // drop the last component of the current path and check if it starts with foundPath 117 | var parentPath = path.split('.').slice(0, -1).join('.'); 118 | if (parentPath.indexOf(foundPath) === 0) { 119 | if (!noCircularOption) { 120 | return typeof options.circular === 'function'? 121 | options.circular(value, path, foundPath): 122 | options.circular; 123 | } 124 | return {$jsan: foundPath}; 125 | } 126 | if (withRefs) return {$jsan: foundPath}; 127 | } 128 | map.set(value, path); 129 | } 130 | 131 | 132 | // If it is an array, replicate the array. 133 | 134 | if (Object.prototype.toString.apply(value) === '[object Array]') { 135 | nu = []; 136 | for (i = 0; i < value.length; i += 1) { 137 | nu[i] = derez(value[i], path + '[' + i + ']', i); 138 | } 139 | } else { 140 | 141 | // If it is an object, replicate the object. 142 | 143 | nu = {}; 144 | for (name in value) { 145 | if (Object.prototype.hasOwnProperty.call(value, name)) { 146 | var nextPath = /^\w+$/.test(name) ? 147 | '.' + name : 148 | '[' + JSON.stringify(name) + ']'; 149 | nu[name] = name === '$jsan' ? [derez(value[name], path + nextPath)] : derez(value[name], path + nextPath, name); 150 | } 151 | } 152 | } 153 | return nu; 154 | } 155 | return value; 156 | }(object, '$')); 157 | }; 158 | 159 | 160 | exports.retrocycle = function retrocycle($) { 161 | 'use strict'; 162 | 163 | 164 | return (function rez(value) { 165 | 166 | // The rez function walks recursively through the object looking for $jsan 167 | // properties. When it finds one that has a value that is a path, then it 168 | // replaces the $jsan object with a reference to the value that is found by 169 | // the path. 170 | 171 | var i, item, name, path; 172 | 173 | if (value && typeof value === 'object') { 174 | if (Object.prototype.toString.apply(value) === '[object Array]') { 175 | for (i = 0; i < value.length; i += 1) { 176 | item = value[i]; 177 | if (item && typeof item === 'object') { 178 | if (item.$jsan) { 179 | value[i] = utils.restore(item.$jsan, $); 180 | } else { 181 | rez(item); 182 | } 183 | } 184 | } 185 | } else { 186 | for (name in value) { 187 | // base case passed raw object 188 | if(typeof value[name] === 'string' && name === '$jsan'){ 189 | return utils.restore(value.$jsan, $); 190 | break; 191 | } 192 | else { 193 | if (name === '$jsan') { 194 | value[name] = value[name][0]; 195 | } 196 | if (typeof value[name] === 'object') { 197 | item = value[name]; 198 | if (item && typeof item === 'object') { 199 | if (item.$jsan) { 200 | value[name] = utils.restore(item.$jsan, $); 201 | } else { 202 | rez(item); 203 | } 204 | } 205 | } 206 | } 207 | } 208 | } 209 | } 210 | return value; 211 | }($)); 212 | }; 213 | -------------------------------------------------------------------------------- /test/unit.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | var jsan = require('../'); 3 | 4 | describe('jsan', function() { 5 | describe('has a stringify method', function() { 6 | it('respects the replacer array argument', function() { 7 | var obj = {a: 1, b: 2}; 8 | var JSONed = JSON.stringify(obj, ['a']); 9 | var jsaned = jsan.stringify(obj, ['a']); 10 | assert.equal(JSONed, jsaned); 11 | }); 12 | 13 | it('respects the replacer function argument', function() { 14 | var obj = {a: 1, b: 2, c: {r: /foo/}}; 15 | var replacer = function(index, value) { 16 | if (value.test) { 17 | return value.toString(); 18 | } 19 | } 20 | var JSONed = JSON.stringify(obj, replacer); 21 | var jsaned = jsan.stringify(obj, replacer); 22 | assert.equal(JSONed, jsaned); 23 | }); 24 | 25 | it('respects the space argument', function() { 26 | var obj = {a: 1, b: 2, c: {foo: 'bar'}}; 27 | var JSONed = JSON.stringify(obj, null, 2); 28 | var jsaned = jsan.stringify(obj, null, 2); 29 | assert.equal(JSONed, jsaned); 30 | }); 31 | 32 | it('behaves the same as JSON.stringify for simple jsonable objects', function() { 33 | var obj = { 34 | a: 1, 35 | b: 'string', 36 | c: [2,3], 37 | d: null 38 | }; 39 | assert.equal(JSON.stringify(obj), jsan.stringify(obj)); 40 | }); 41 | 42 | it('uses the toJSON() method when possible', function() { 43 | var obj = { a: { b: 1, toJSON: function(key) { return key } } }; 44 | assert.equal(jsan.stringify(obj, null, null, false), '{"a":"a"}'); 45 | }); 46 | 47 | it('can handle dates', function() { 48 | var obj = { 49 | now: new Date() 50 | } 51 | var str = jsan.stringify(obj, null, null, true); 52 | assert(/^\{"now":\{"\$jsan":"d[^"]*"\}\}$/.test(str)); 53 | }); 54 | 55 | it('can handle regexes', function() { 56 | var obj = { 57 | r: /test/ 58 | } 59 | var str = jsan.stringify(obj, null, null, true); 60 | assert.deepEqual(str, '{"r":{"$jsan":"r,test"}}'); 61 | }); 62 | 63 | it('can handle functions', function() { 64 | var obj = { 65 | f: function () {} 66 | } 67 | var str = jsan.stringify(obj, null, null, true); 68 | assert.deepEqual(str, '{"f":{"$jsan":"ffunction () { /* ... */ }"}}'); 69 | }); 70 | 71 | it('can handle undefined', function() { 72 | var obj = undefined; 73 | var str = jsan.stringify(obj, null, null, true); 74 | assert.deepEqual(str, '{"$jsan":"u"}'); 75 | }); 76 | 77 | it('can handle NaN', function() { 78 | var obj = NaN; 79 | var str = jsan.stringify(obj, null, null, true); 80 | assert.deepEqual(str, '{"$jsan":"n"}'); 81 | }); 82 | 83 | it('can handle Infinity', function() { 84 | var obj = Infinity; 85 | var str = jsan.stringify(obj, null, null, true); 86 | assert.deepEqual(str, '{"$jsan":"i"}'); 87 | }); 88 | 89 | it('can handle -Infinity', function() { 90 | var obj = -Infinity; 91 | var str = jsan.stringify(obj, null, null, true); 92 | assert.deepEqual(str, '{"$jsan":"y"}'); 93 | }); 94 | 95 | it('can handle nested undefined', function() { 96 | var obj = { 97 | u: undefined 98 | } 99 | var str = jsan.stringify(obj, null, null, true); 100 | assert.deepEqual(str, '{"u":{"$jsan":"u"}}'); 101 | }); 102 | 103 | it('can handle nested NaN', function() { 104 | var obj = { 105 | u: NaN 106 | } 107 | var str = jsan.stringify(obj, null, null, true); 108 | assert.deepEqual(str, '{"u":{"$jsan":"n"}}'); 109 | }); 110 | 111 | it('can handle nested Infinity', function() { 112 | var obj = { 113 | u: Infinity 114 | } 115 | var str = jsan.stringify(obj, null, null, true); 116 | assert.deepEqual(str, '{"u":{"$jsan":"i"}}'); 117 | }); 118 | 119 | it('can handle nested -Infinity', function() { 120 | var obj = { 121 | u: -Infinity 122 | } 123 | var str = jsan.stringify(obj, null, null, true); 124 | assert.deepEqual(str, '{"u":{"$jsan":"y"}}'); 125 | }); 126 | 127 | it('can handle errors', function() { 128 | var obj = { 129 | e: new Error(':(') 130 | } 131 | var str = jsan.stringify(obj, null, null, true); 132 | assert.deepEqual(str, '{"e":{"$jsan":"e:("}}'); 133 | }); 134 | 135 | if (typeof Symbol !== 'undefined') { 136 | it('can handle ES symbols', function() { 137 | var obj = { 138 | s: Symbol('a') 139 | } 140 | var str = jsan.stringify(obj, null, null, true); 141 | assert.deepEqual(str, '{"s":{"$jsan":"sa"}}'); 142 | }); 143 | 144 | it('can handle global ES symbols', function() { 145 | var obj = { 146 | g: Symbol.for('a') 147 | } 148 | var str = jsan.stringify(obj, null, null, true); 149 | assert.deepEqual(str, '{"g":{"$jsan":"ga"}}'); 150 | }); 151 | } 152 | 153 | if (typeof Map !== 'undefined' && typeof Array.from !== 'undefined') { 154 | it('can handle ES Map', function() { 155 | var obj = { 156 | map: new Map([ 157 | ['a', 1], 158 | [{toString: function (){ return 'a' }}, 2], 159 | [{}, 3] 160 | ]) 161 | } 162 | var str = jsan.stringify(obj, null, null, true); 163 | assert.deepEqual(str, '{"map":{"$jsan":"m[[\\"a\\",1],[{\\"toString\\":{\\"$jsan\\":\\"ffunction (){ /* ... */ }\\"}},2],[{},3]]"}}'); 164 | }); 165 | } 166 | 167 | if (typeof Set !== 'undefined' && typeof Array.from !== 'undefined') { 168 | it('can handle ES Set', function() { 169 | var obj = { 170 | set: new Set(['a', {toString: function (){ return 'a' }}, {}]) 171 | } 172 | var str = jsan.stringify(obj, null, null, true); 173 | assert.deepEqual(str, '{"set":{"$jsan":"l[\\"a\\",{\\"toString\\":{\\"$jsan\\":\\"ffunction (){ /* ... */ }\\"}},{}]"}}'); 174 | }); 175 | } 176 | 177 | it('works on objects with circular references', function() { 178 | var obj = {}; 179 | obj['self'] = obj; 180 | assert.equal(jsan.stringify(obj), '{"self":{"$jsan":"$"}}'); 181 | }); 182 | 183 | it('can use the circular option', function() { 184 | var obj = {}; 185 | obj.self = obj; 186 | obj.a = 1; 187 | obj.b = {}; 188 | obj.c = obj.b; 189 | assert.equal(jsan.stringify(obj, null, null, {circular: '∞'}), '{"self":"∞","a":1,"b":{},"c":{"$jsan":"$.b"}}'); 190 | assert.equal(jsan.stringify(obj, null, null, {circular: function() { return '∞!' }}), '{"self":"∞!","a":1,"b":{},"c":{"$jsan":"$.b"}}'); 191 | }); 192 | 193 | it('can use the refs option', function() { 194 | var obj1 = { a: 1 }; 195 | var obj = { "prop1": obj1, "prop2": { "prop3": obj1 } }; 196 | assert.equal(jsan.stringify(obj, null, null, {refs: true}), '{"prop1":{"a":1},"prop2":{"prop3":{"$jsan":"$.prop1"}}}'); 197 | assert.equal(jsan.stringify(obj, null, null, true), '{"prop1":{"a":1},"prop2":{"prop3":{"$jsan":"$.prop1"}}}'); 198 | assert.equal(jsan.stringify(obj, null, null, false), '{"prop1":{"a":1},"prop2":{"prop3":{"$jsan":"$.prop1"}}}'); 199 | assert.equal(jsan.stringify(obj, null, null, {refs: false}), '{"prop1":{"a":1},"prop2":{"prop3":{"a":1}}}'); 200 | }); 201 | 202 | it('works with refs option with circular references', function() { 203 | var obj = {}; 204 | obj.self = obj; 205 | obj.a = 1; 206 | obj.b = {t: 1}; 207 | obj.c = obj.b; 208 | assert.equal(jsan.stringify(obj, null, null, {refs: false}), '{"self":{"$jsan":"$"},"a":1,"b":{"t":1},"c":{"t":1}}'); 209 | assert.equal(jsan.stringify(obj, null, null, {refs: false, circular: 'Circular'}), '{"self":"Circular","a":1,"b":{"t":1},"c":{"t":1}}'); 210 | }); 211 | 212 | it('works on objects with "[", "\'", and "]" in the keys', function() { 213 | var obj = {}; 214 | obj['["key"]'] = {}; 215 | obj['["key"]']['["key"]'] = obj['["key"]']; 216 | assert.equal(jsan.stringify(obj), '{"[\\"key\\"]":{"[\\"key\\"]":{"$jsan":"$[\\"[\\\\\\"key\\\\\\"]\\"]"}}}'); 217 | }); 218 | 219 | it('works on objects that will get encoded with \\uXXXX', function() { 220 | var obj = {"\u017d\u010d":{},"kraj":"\u017du\u017e"}; 221 | obj["\u017d\u010d"]["\u017d\u010d"] = obj["\u017d\u010d"]; 222 | assert.equal(jsan.stringify(obj), '{"\u017d\u010d":{"\u017d\u010d":{"$jsan":"$[\\\"\u017d\u010d\\\"]"}},"kraj":"Žuž"}'); 223 | }); 224 | 225 | it('works on circular arrays', function() { 226 | var obj = []; 227 | obj[0] = []; 228 | obj[0][0] = obj[0]; 229 | assert.equal(jsan.stringify(obj), '[[{"$jsan":"$[0]"}]]'); 230 | }); 231 | 232 | it('works correctly for mutiple calls with the same object', function() { 233 | var obj = {}; 234 | obj.self = obj; 235 | obj.a = {}; 236 | obj.b = obj.a; 237 | assert.equal(jsan.stringify(obj), '{"self":{"$jsan":"$"},"a":{},"b":{"$jsan":"$.a"}}'); 238 | assert.equal(jsan.stringify(obj), '{"self":{"$jsan":"$"},"a":{},"b":{"$jsan":"$.a"}}'); 239 | }); 240 | 241 | it('does not report false positives for circular references', function() { 242 | /** 243 | * This is a test for an edge case in which jsan.stringify falsely reported circular references 244 | * The minimal conditions are 245 | * 1) The object has an error preventing serialization by json.stringify 246 | * 2) The object contains a repeated reference 247 | * 3) the second path of the reference contains the first path 248 | */ 249 | var circular = {}; 250 | circular.self = circular; 251 | 252 | var ref = {}; 253 | 254 | var obj = { 255 | circularRef: circular, 256 | ref: ref, 257 | refAgain: ref, 258 | }; 259 | 260 | var result = jsan.stringify(obj, null, null, { circular: "[CIRCULAR]" }); 261 | assert.equal('{"circularRef":{"self":"[CIRCULAR]"},"ref":{},"refAgain":{"$jsan":"$.ref"}}', result) 262 | }); 263 | }); 264 | 265 | 266 | 267 | describe('has a parse method', function() { 268 | it('behaves the same as JSON.parse for valid json strings', function() { 269 | var str = '{"a":1,"b":"string","c":[2,3],"d":null}'; 270 | assert.deepEqual(JSON.parse(str), jsan.parse(str)); 271 | }); 272 | 273 | it('uses reviver function', function() { 274 | var str = '{"a":1,"b":null}'; 275 | assert.deepEqual({"a":1,"b":2}, 276 | jsan.parse(str, function (key, value) { return key === "b" ? 2 : value; }) 277 | ); 278 | }); 279 | 280 | it('can decode dates', function() { 281 | var str = '{"$jsan":"d1400000000000"}'; 282 | var obj = jsan.parse(str); 283 | assert(obj instanceof Date); 284 | }); 285 | 286 | it('can decode dates while using reviver', function() { 287 | var str = '{"$jsan":"d1400000000000"}'; 288 | var obj = jsan.parse(str, function (key, value) { return value; }); 289 | assert(obj instanceof Date); 290 | }); 291 | 292 | it('can decode regexes', function() { 293 | str = '{"$jsan":"r,test"}'; 294 | var obj = jsan.parse(str); 295 | assert(obj instanceof RegExp ) 296 | }); 297 | 298 | it('can decode functions', function() { 299 | str = '{"$jsan":"ffunction () { /* ... */ }"}'; 300 | var obj = jsan.parse(str); 301 | assert(obj instanceof Function); 302 | }); 303 | 304 | it('can decode undefined', function() { 305 | str = '{"$jsan":"u"}'; 306 | var obj = jsan.parse(str); 307 | assert(obj === undefined); 308 | }); 309 | 310 | it('can decode NaN', function() { 311 | str = '{"$jsan":"n"}'; 312 | var obj = jsan.parse(str); 313 | assert(isNaN(obj) && typeof obj === 'number'); 314 | }); 315 | 316 | it('can decode Infinity', function() { 317 | str = '{"$jsan":"i"}'; 318 | var obj = jsan.parse(str); 319 | assert(obj === Number.POSITIVE_INFINITY); 320 | }); 321 | 322 | it('can decode -Infinity', function() { 323 | str = '{"$jsan":"y"}'; 324 | var obj = jsan.parse(str); 325 | assert(obj === Number.NEGATIVE_INFINITY); 326 | }); 327 | 328 | it('can decode errors', function() { 329 | str = '{"$jsan":"e:("}'; 330 | var obj = jsan.parse(str); 331 | assert(obj instanceof Error && obj.message === ':('); 332 | }); 333 | 334 | it('can decode nested dates', function() { 335 | var str = '{"now":{"$jsan":"d1400000000000"}}'; 336 | var obj = jsan.parse(str); 337 | assert(obj.now instanceof Date); 338 | }); 339 | 340 | it('can decode nested regexes', function() { 341 | str = '{"r":{"$jsan":"r,test"}}'; 342 | var obj = jsan.parse(str); 343 | assert(obj.r instanceof RegExp ) 344 | }); 345 | 346 | it('can decode nested functions', function() { 347 | str = '{"f":{"$jsan":"ffunction () { /* ... */ }"}}'; 348 | var obj = jsan.parse(str); 349 | assert(obj.f instanceof Function); 350 | }); 351 | 352 | it('can decode nested undefined', function() { 353 | str = '{"u":{"$jsan":"u"}}'; 354 | var obj = jsan.parse(str); 355 | assert('u' in obj && obj.u === undefined); 356 | }); 357 | 358 | it('can decode nested NaN', function() { 359 | str = '{"u":{"$jsan":"n"}}'; 360 | var obj = jsan.parse(str); 361 | assert('u' in obj && isNaN(obj.u) && typeof obj.u === 'number'); 362 | }); 363 | 364 | it('can decode nested Infinity', function() { 365 | str = '{"u":{"$jsan":"i"}}'; 366 | var obj = jsan.parse(str); 367 | assert('u' in obj && obj.u === Number.POSITIVE_INFINITY); 368 | }); 369 | 370 | it('can decode nested -Infinity', function() { 371 | str = '{"u":{"$jsan":"y"}}'; 372 | var obj = jsan.parse(str); 373 | assert('u' in obj && obj.u === Number.NEGATIVE_INFINITY); 374 | }); 375 | 376 | it('can decode nested errors', function() { 377 | str = '{"e":{"$jsan":"e:("}}'; 378 | var obj = jsan.parse(str); 379 | assert(obj.e instanceof Error && obj.e.message === ':('); 380 | }); 381 | 382 | if (typeof Symbol !== 'undefined') { 383 | it('can decode ES symbols', function() { 384 | str = '{"s1":{"$jsan":"sfoo"}, "s2":{"$jsan":"s"}}'; 385 | var obj = jsan.parse(str); 386 | assert(typeof obj.s1 === 'symbol' && obj.s1.toString() === 'Symbol(foo)'); 387 | assert(typeof obj.s2 === 'symbol' && obj.s2.toString() === 'Symbol()'); 388 | }); 389 | 390 | it('can decode global ES symbols', function() { 391 | str = '{"g1":{"$jsan":"gfoo"}, "g2":{"$jsan":"gundefined"}}'; 392 | var obj = jsan.parse(str); 393 | assert(typeof obj.g1 === 'symbol' && obj.g1 === Symbol.for('foo')); 394 | assert(typeof obj.g2 === 'symbol' && obj.g2 === Symbol.for()); 395 | }); 396 | } 397 | 398 | if (typeof Map !== 'undefined' && typeof Array.from !== 'undefined') { 399 | it('can decode ES Map', function() { 400 | var str = '{"map":{"$jsan":"m[[\\"a\\",1],[{\\"toString\\":{\\"$jsan\\":\\"ffunction (){ /* ... */ }\\"}},2],[{},3]]"}}'; 401 | var obj = jsan.parse(str); 402 | var keys = obj.map.keys(); 403 | var values = obj.map.values(); 404 | assert.equal(keys.next().value, 'a'); 405 | assert.equal(typeof keys.next().value.toString, 'function'); 406 | assert.equal(typeof keys.next().value, 'object'); 407 | assert.equal(values.next().value, 1); 408 | assert.equal(values.next().value, 2); 409 | assert.equal(values.next().value, 3); 410 | }); 411 | } 412 | 413 | if (typeof Set !== 'undefined' && typeof Array.from !== 'undefined') { 414 | it('can decode ES Set', function() { 415 | var str = '{"set":{"$jsan":"l[\\"a\\",{\\"toString\\":{\\"$jsan\\":\\"ffunction (){ /* ... */ }\\"}},{}]"}}'; 416 | var obj = jsan.parse(str); 417 | var values = obj.set.values(); 418 | assert.equal(values.next().value, 'a'); 419 | assert.equal(typeof values.next().value.toString, 'function'); 420 | assert.equal(typeof values.next().value, 'object'); 421 | }); 422 | } 423 | 424 | it('works on object strings with a circular dereferences', function() { 425 | var str = '{"a":1,"b":"string","c":[2,3],"d":null,"self":{"$jsan":"$"}}'; 426 | var obj = jsan.parse(str); 427 | assert.deepEqual(obj['self'], obj); 428 | }); 429 | 430 | it('works on object strings with "[", "\'", and "]" in the keys', function() { 431 | var str = '{"[\\"key\\"]":{"[\\"key\\"]":{"$jsan":"$[\\"[\\\\\\"key\\\\\\"]\\"]"}}}'; 432 | var obj = jsan.parse(str); 433 | assert.deepEqual(obj['["key"]']['["key"]'], obj['["key"]']); 434 | }); 435 | 436 | it('works on objects encoded with \\uXXXX', function() { 437 | var str = '{"\u017d\u010d":{"\u017d\u010d":{"$jsan":"$[\\\"\\u017d\\u010d\\\"]"}},"kraj":"Žuž"}'; 438 | var obj = jsan.parse(str); 439 | assert.deepEqual(obj["\u017d\u010d"]["\u017d\u010d"], obj["\u017d\u010d"]); 440 | }); 441 | 442 | it('works on array strings with circular dereferences', function() { 443 | var str = '[[{"$jsan":"$[0]"}]]'; 444 | var arr = jsan.parse(str); 445 | assert.deepEqual(arr[0][0], arr[0]); 446 | }); 447 | }); 448 | 449 | }); 450 | --------------------------------------------------------------------------------