├── .gitignore ├── .npmignore ├── .travis.yml ├── LICENSE ├── README.md ├── custom.js ├── index.js ├── package.json └── test └── tests.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 18 | .grunt 19 | 20 | # node-waf configuration 21 | .lock-wscript 22 | 23 | # Compiled binary addons (http://nodejs.org/api/addons.html) 24 | build/Release 25 | 26 | # Dependency directory 27 | node_modules 28 | 29 | # Optional npm cache directory 30 | .npm 31 | 32 | # Optional REPL history 33 | .node_repl_history 34 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /test 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "9" 4 | - "8" 5 | - "7" 6 | - "6" 7 | - "5" 8 | - "4" 9 | - "10" 10 | 11 | sudo: false 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Ben Newman 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # arson [![Build Status](https://travis-ci.org/benjamn/arson.svg?branch=master)](https://travis-ci.org/benjamn/arson) [![Greenkeeper badge](https://badges.greenkeeper.io/benjamn/arson.svg)](https://greenkeeper.io/) 2 | 3 | ### *AR*bitrary *S*tructured *O*bject *N*otation 4 | 5 | _Not to be confused with the criminal act of deliberately setting fire to property!_ 6 | 7 | [JSON](http://www.json.org/) is great until you need to encode an object with circular references: 8 | ```js 9 | var obj = {}; 10 | obj.self = obj; 11 | JSON.stringify(obj); // throws 12 | ``` 13 | 14 | Throwing an exception is lame, but even worse is muddling along as if everything is ok: 15 | ```js 16 | var a = {}; 17 | var b = { foo: 42 }; 18 | a.x = a.y = b; 19 | var c = JSON.parse(JSON.stringify(a)); 20 | assert.strictEqual(c.x, c.y); // fails 21 | ``` 22 | 23 | We need an object notation that supports circular and repeated references. 24 | 25 | That's where `ARSON` comes in: 26 | ```js 27 | var a = {}; 28 | var b = { foo: 42 }; 29 | a.x = a.y = b; 30 | var c = ARSON.parse(ARSON.stringify(a)); 31 | assert.strictEqual(c.x, c.y); // no problem! 32 | ``` 33 | 34 | `ARSON` is compact, often even more compact than JSON, because repeated objects are defined only once: 35 | ```js 36 | var a = {}; 37 | var b = { foo: 42 }; 38 | a.x = a.y = b; 39 | ARSON.stringify(a); // [{"x":1,"y":1},{"foo":2},42] vs. 40 | // {"x":{"foo":42},"y":{"foo":42}} 41 | ``` 42 | 43 | But that's not all! `ARSON` can also encode `undefined`, thanks to the fact that `[][-1]` is always `undefined`: 44 | ```js 45 | > ARSON.encode({foo:undefined}) 46 | '[{"foo":-1}]' 47 | > ARSON.decode(_) 48 | { foo: undefined } 49 | ``` 50 | 51 | It can also encode array *holes*: 52 | ```js 53 | > ARSON.encode(Array(3).concat([4, 5])) 54 | '[[-2,-2,-2,1,2],4,5]' 55 | > ARSON.decode(_) 56 | [ , , , 4, 5 ] 57 | ``` 58 | 59 | `Buffer`s: 60 | ```js 61 | > ARSON.encode(new Buffer("asdf")) 62 | '[["Buffer","YXNkZg==","base64"]]' 63 | > ARSON.decode(_) 64 | 65 | ``` 66 | 67 | `Date`s: 68 | ```js 69 | > ARSON.encode(new Date) 70 | '[["Date","2016-02-02T00:25:36.886Z"]]' 71 | > ARSON.decode(_) 72 | Mon Feb 01 2016 19:25:36 GMT-0500 (EST) 73 | ``` 74 | 75 | `RegExp`s: 76 | ```js 77 | > ARSON.encode(/asdf/img) 78 | '[["RegExp","asdf","img"]]' 79 | > ARSON.decode(_) 80 | /asdf/gim 81 | ``` 82 | 83 | `Set`s: 84 | ```js 85 | > s = new Set 86 | Set {} 87 | > s.add(s) 88 | Set { Set { Set { [Object] } } } 89 | > ARSON.encode(s) 90 | '[["Set",0]]' 91 | > ARSON.decode(_) 92 | Set { Set { Set { [Object] } } } 93 | > _.has(_) 94 | true 95 | ``` 96 | 97 | and `Map`s: 98 | ```js 99 | > m = new Map 100 | Map {} 101 | > m.set(1234, m) 102 | Map { 1234 => Map { 1234 => Map { 1234 => [Object] } } } 103 | > m.set(m, 5678) 104 | Map { 105 | 1234 => Map { 106 | 1234 => Map { 107 | 1234 => [Object], 108 | [Object] => 5678 109 | }, 110 | Map { 111 | 1234 => [Object], 112 | [Object] => 5678 113 | } => 5678 114 | }, 115 | Map { 116 | 1234 => Map { 117 | 1234 => [Object], 118 | [Object] => 5678 119 | }, 120 | Map { 121 | 1234 => [Object], 122 | [Object] => 5678 123 | } => 5678 124 | } => 5678 125 | } 126 | > ARSON.encode(m) 127 | '[["Map",1,2],[3,0],[0,4],1234,5678]' 128 | > ARSON.decode(_) 129 | Map { 130 | 1234 => Map { 131 | 1234 => Map { 132 | 1234 => [Object], 133 | [Object] => 5678 134 | }, 135 | Map { 136 | 1234 => [Object], 137 | [Object] => 5678 138 | } => 5678 139 | }, 140 | Map { 141 | 1234 => Map { 142 | 1234 => [Object], 143 | [Object] => 5678 144 | }, 145 | Map { 146 | 1234 => [Object], 147 | [Object] => 5678 148 | } => 5678 149 | } => 5678 150 | } 151 | > _.get(_.get(1234)) === 5678 152 | true 153 | ``` 154 | -------------------------------------------------------------------------------- /custom.js: -------------------------------------------------------------------------------- 1 | var toString = Object.prototype.toString; 2 | var dateTag = "[object Date]"; 3 | var regExpTag = "[object RegExp]"; 4 | var setTag = "[object Set]"; 5 | var mapTag = "[object Map]"; 6 | 7 | var arson = require("./index.js"); 8 | 9 | typeof Buffer === "function" && 10 | typeof Buffer.isBuffer === "function" && 11 | arson.registerType("Buffer", { 12 | deconstruct: function (buf) { 13 | return Buffer.isBuffer(buf) && [buf.toString("base64"), "base64"]; 14 | }, 15 | 16 | // The reconstruct function will be called twice: once with no 17 | // arguments, which allows it to return a placeholder object reference; 18 | // and once with one argument, a copy of the array returned by the 19 | // deconstruct function. For immutable types like Buffer, Date, and 20 | // RegExp, the reconstruct function should return a falsy value when it 21 | // receives no arguments, since there is no way to create an empty 22 | // Buffer or Date and later fill in its contents. For container types 23 | // like Map and Set, the reconstruct function must return an empty 24 | // instance of the container when it receives no arguments, so that we 25 | // can fill in that empty container later. This two-phased strategy is 26 | // essential for decoding containers that contain themselves. 27 | reconstruct: function (args) { 28 | return args && new Buffer(args[0], args[1]); 29 | } 30 | }); 31 | 32 | arson.registerType("Date", { 33 | deconstruct: function (date) { 34 | return toString.call(date) === dateTag && [date.toJSON()]; 35 | }, 36 | 37 | reconstruct: function (args) { 38 | return args && new Date(args[0]); 39 | } 40 | }); 41 | 42 | arson.registerType("RegExp", { 43 | deconstruct: function (exp) { 44 | if (toString.call(exp) === regExpTag) { 45 | var args = [exp.source]; 46 | var flags = ""; 47 | 48 | if (exp.ignoreCase) flags += "i"; 49 | if (exp.multiline) flags += "m"; 50 | if (exp.global) flags += "g"; 51 | 52 | if (flags) { 53 | args.push(flags); 54 | } 55 | 56 | return args; 57 | } 58 | }, 59 | 60 | reconstruct: function (args) { 61 | return args && new RegExp(args[0], args[1]); 62 | } 63 | }); 64 | 65 | typeof Set === "function" && 66 | typeof Array.from === "function" && 67 | arson.registerType("Set", { 68 | deconstruct: function (set) { 69 | if (toString.call(set) === setTag) { 70 | return Array.from(set); 71 | } 72 | }, 73 | 74 | reconstruct: function (values) { 75 | if (values) { 76 | values.forEach(this.add, this); 77 | } else { 78 | return new Set; 79 | } 80 | } 81 | }); 82 | 83 | typeof Map === "function" && 84 | typeof Array.from === "function" && 85 | arson.registerType("Map", { 86 | deconstruct: function (map) { 87 | if (toString.call(map) === mapTag) { 88 | return Array.from(map); 89 | } 90 | }, 91 | 92 | reconstruct: function (entries) { 93 | if (entries) { 94 | entries.forEach(function (entry) { 95 | this.set(entry[0], entry[1]); 96 | }, this); 97 | } else { 98 | return new Map; 99 | } 100 | } 101 | }); 102 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var UNDEFINED_INDEX = -1; 2 | var ARRAY_HOLE_INDEX = -2; 3 | var NAN_INDEX = -3; 4 | var POS_INF_INDEX = -4; 5 | var NEG_INF_INDEX = -5; 6 | var customTypes = Object.create(null); 7 | 8 | exports.registerType = function (typeName, handlers) { 9 | function check(methodName) { 10 | if (typeof handlers[methodName] !== "function") { 11 | throw new Error( 12 | "second argument to ARSON.registerType(" + 13 | JSON.stringify(typeName) + ", ...) " + 14 | "must be an object with a " + methodName + " method" 15 | ); 16 | } 17 | } 18 | 19 | check("deconstruct"); 20 | check("reconstruct"); 21 | 22 | customTypes[typeName] = handlers; 23 | 24 | return exports; 25 | }; 26 | 27 | require("./custom.js"); 28 | 29 | exports.encode = exports.stringify = 30 | function encode(value) { 31 | return JSON.stringify(toTable(value)); 32 | } 33 | 34 | // This array will grow as needed so that we can slice arrays filled with 35 | // ARRAY_HOLE_INDEX from it. 36 | var HOLY_ARRAY = []; 37 | 38 | // Returns an array of the given length filled with ARRAY_HOLE_INDEX. 39 | function getArrayOfHoles(length) { 40 | var holyLen = HOLY_ARRAY.length; 41 | if (length > holyLen) { 42 | HOLY_ARRAY.length = length; 43 | for (var i = holyLen; i < length; ++i) { 44 | HOLY_ARRAY[i] = ARRAY_HOLE_INDEX; 45 | } 46 | } 47 | 48 | return HOLY_ARRAY.slice(0, length); 49 | } 50 | 51 | function toTable(value) { 52 | var values = []; 53 | var getIndex = makeGetIndexFunction(values); 54 | 55 | function copy(value) { 56 | var result = value; 57 | 58 | if (value && typeof value === "object") { 59 | var keys = Object.keys(value); 60 | 61 | if (isPlainObject(value)) { 62 | result = {}; 63 | 64 | } else if (Array.isArray(value)) { 65 | result = getArrayOfHoles(value.length); 66 | 67 | } else { 68 | for (var typeName in customTypes) { 69 | // If value is not a plain Object, but something exotic like a 70 | // Date or a RegExp, serialize it as an array with typeName as 71 | // its first element. These arrays can be distinguished from 72 | // normal arrays, because all other non-empty arrays will be 73 | // serialized with a numeric value as their first element. 74 | var args = customTypes[typeName].deconstruct(value); 75 | if (args) { 76 | for (var i = 0; i < args.length; ++i) { 77 | args[i] = getIndex(args[i]); 78 | } 79 | args.unshift(typeName); 80 | return args; 81 | } 82 | } 83 | 84 | result = {}; 85 | } 86 | 87 | keys.forEach(function (key) { 88 | result[key] = getIndex(value[key]); 89 | }); 90 | } 91 | 92 | return result; 93 | } 94 | 95 | // Assigns the root value to values[0]. 96 | var index0 = getIndex(value); 97 | if (index0 < 0) { 98 | // If value is something special that gets a negative index, then we 99 | // don't need to build a table at all, and we can simply return that 100 | // negative index as a complete serialization. This avoids ambiguity 101 | // about indexes versus primitive literal values. 102 | return index0; 103 | } 104 | 105 | // Note that this for loop cannot be a forEach loop, because 106 | // values.length is expected to change during iteration. 107 | for (var table = [], v = 0; v < values.length; ++v) { 108 | table[v] = copy(values[v]); 109 | } 110 | 111 | return table; 112 | } 113 | 114 | function isPlainObject(value) { 115 | var isObject = value && typeof value === "object"; 116 | if (isObject) { 117 | var proto = Object.getPrototypeOf 118 | ? Object.getPrototypeOf(value) 119 | : value.__proto__; 120 | return proto === Object.prototype; 121 | } 122 | return false; 123 | } 124 | 125 | function makeGetIndexFunction(values) { 126 | var indexMap = typeof Map === "function" && new Map; 127 | 128 | return function getIndex(value) { 129 | switch (typeof value) { 130 | case "undefined": 131 | return UNDEFINED_INDEX; 132 | 133 | case "number": 134 | if (isNaN(value)) { 135 | return NAN_INDEX; 136 | } 137 | 138 | if (! isFinite(value)) { 139 | return value < 0 ? NEG_INF_INDEX : POS_INF_INDEX; 140 | } 141 | 142 | // fall through... 143 | } 144 | 145 | var index; 146 | 147 | if (indexMap) { 148 | // If we have Map, use it instead of values.indexOf to accelerate 149 | // object lookups. 150 | index = indexMap.get(value); 151 | if (typeof index === "undefined") { 152 | index = values.push(value) - 1; 153 | indexMap.set(value, index); 154 | } 155 | } else { 156 | index = values.indexOf(value); 157 | if (index < 0) { 158 | index = values.push(value) - 1; 159 | } 160 | } 161 | 162 | return index; 163 | }; 164 | } 165 | 166 | exports.decode = exports.parse = 167 | function decode(encoding) { 168 | return fromTable(JSON.parse(encoding)); 169 | } 170 | 171 | function fromTable(table) { 172 | if (typeof table === "number" && table < 0) { 173 | return getValueWithoutCache(table); 174 | } 175 | 176 | var getValueCache = new Array(table.length); 177 | 178 | function getValue(index) { 179 | return index in getValueCache 180 | ? getValueCache[index] 181 | : getValueCache[index] = getValueWithoutCache(index); 182 | } 183 | 184 | function getValueWithoutCache(index) { 185 | if (index < 0) { 186 | if (index === UNDEFINED_INDEX) { 187 | return; 188 | } 189 | 190 | if (index === ARRAY_HOLE_INDEX) { 191 | // Never reached because handled specially below. 192 | return; 193 | } 194 | 195 | if (index === NAN_INDEX) { 196 | return NaN; 197 | } 198 | 199 | if (index === POS_INF_INDEX) { 200 | return Infinity; 201 | } 202 | 203 | if (index === NEG_INF_INDEX) { 204 | return -Infinity; 205 | } 206 | 207 | throw new Error("invalid ARSON index: " + index); 208 | } 209 | 210 | var entry = table[index]; 211 | 212 | if (entry && typeof entry === "object") { 213 | if (Array.isArray(entry)) { 214 | var elem0 = entry[0]; 215 | if (typeof elem0 === "string" && elem0 in customTypes) { 216 | var rec = customTypes[elem0].reconstruct; 217 | var empty = rec(); 218 | if (empty) { 219 | // If the reconstruct handler returns an object, treat it as 220 | // an empty instance of the desired type, and schedule it to 221 | // be filled in later. This two-stage process allows exotic 222 | // container objects to contain themselves. 223 | containers.push({ 224 | reconstruct: rec, 225 | empty: empty, 226 | argIndexes: entry.slice(1) 227 | }); 228 | } 229 | 230 | // If the reconstruct handler returned a falsy value, then we 231 | // assume none of its arguments refer to exotic containers, so 232 | // we can reconstruct the object immediately. Examples: Buffer, 233 | // Date, RegExp. 234 | return table[index] = empty || rec(entry.slice(1).map(getValue)); 235 | } 236 | } 237 | 238 | // Here entry is already the correct array or object reference for 239 | // this index, but its values are still indexes that will need to be 240 | // resolved later. 241 | objects.push(entry); 242 | } 243 | 244 | return entry; 245 | } 246 | 247 | var containers = []; 248 | var objects = []; 249 | 250 | // First pass: make sure all exotic objects are deserialized fist, and 251 | // keep track of all plain object entries for later. 252 | table.forEach(function (entry, i) { 253 | getValue(i); 254 | }); 255 | 256 | // Second pass: now that we have final object references for all exotic 257 | // objects, we can safely resolve argument indexes for the empty ones. 258 | containers.forEach(function (c) { 259 | c.args = c.argIndexes.map(getValue); 260 | }); 261 | 262 | // Third pass: resolve value indexes for ordinary arrays and objects. 263 | objects.forEach(function (obj) { 264 | Object.keys(obj).forEach(function (key) { 265 | var index = obj[key]; 266 | 267 | if (typeof index !== "number") { 268 | // Leave non-numeric indexes untouched. 269 | return; 270 | } 271 | 272 | if (index < 0) { 273 | if (index === ARRAY_HOLE_INDEX) { 274 | // Array holes have to be handled specially here, since getValue 275 | // does not have a reference to obj. 276 | delete obj[key]; 277 | return; 278 | } 279 | 280 | // This recursion is guaranteed not to add more objects, because 281 | // we know the index is negative. 282 | obj[key] = getValue(index); 283 | 284 | } else { 285 | // Non-negative indexes refer to normal table values. 286 | obj[key] = table[index]; 287 | } 288 | }); 289 | }); 290 | 291 | // Fourth pass: all possible object references have been established, so 292 | // we can finally initialize the empty container objects. 293 | containers.forEach(function (c) { 294 | c.reconstruct.call(c.empty, c.args); 295 | }); 296 | 297 | return table[0]; 298 | } 299 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "arson", 3 | "version": "0.2.6", 4 | "description": "ARbitrary Structured Object Notation", 5 | "main": "index.js", 6 | "repository": { 7 | "type": "git", 8 | "url": "git+https://github.com/benjamn/arson.git" 9 | }, 10 | "author": "Ben Newman ", 11 | "license": "MIT", 12 | "bugs": { 13 | "url": "https://github.com/benjamn/arson/issues" 14 | }, 15 | "homepage": "https://github.com/benjamn/arson#readme", 16 | "scripts": { 17 | "test": "node ./node_modules/mocha/bin/mocha --reporter spec --full-trace", 18 | "debug": "node ./node_modules/mocha/bin/mocha --debug-brk --reporter spec --full-trace" 19 | }, 20 | "devDependencies": { 21 | "mocha": "~5.2.0", 22 | "immutable-tuple": "^0.4.2" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /test/tests.js: -------------------------------------------------------------------------------- 1 | var assert = require("assert"); 2 | var arson = require("../index.js"); 3 | 4 | describe("encoding and decoding", function () { 5 | it("should work with primitive values", function () { 6 | function check(value) { 7 | var enc = arson.encode(value); 8 | var dec = arson.decode(enc); 9 | 10 | if (isNaN(value)) { 11 | assert.ok(isNaN(dec)); 12 | } else { 13 | assert.deepEqual(value, dec); 14 | } 15 | } 16 | 17 | check(0); 18 | check(1234); 19 | check(NaN); 20 | check(Infinity); 21 | check(-Infinity); 22 | check(true); 23 | check(false); 24 | check("asdf"); 25 | check(""); 26 | check(null); 27 | check(void 0); 28 | check({ foo: void 0 }); 29 | }); 30 | 31 | it("should work with RegExp objects", function () { 32 | var r1 = /asdf/ig; 33 | var r2 = arson.decode(arson.encode(r1)); 34 | assert.ok(r2 instanceof RegExp); 35 | assert.strictEqual(r2.source, "asdf"); 36 | assert.strictEqual(r2.ignoreCase, true); 37 | assert.strictEqual(r2.multiline, false); 38 | assert.strictEqual(r2.global, true); 39 | assert.ok(r2.test("xxx-asdf-yyy")); 40 | }); 41 | 42 | it("should work with Date objects", function () { 43 | var d1 = new Date; 44 | var d2 = arson.decode(arson.encode(d1)); 45 | assert.ok(d2 instanceof Date); 46 | assert.strictEqual(+d1, +d2); 47 | 48 | var dObj = arson.decode(arson.encode({ foo: d1, bar: [d1, d1] })); 49 | assert.strictEqual(+d1, +dObj.foo); 50 | assert.strictEqual(+d1, +dObj.bar[0]); 51 | assert.strictEqual(+d1, +dObj.bar[1]); 52 | assert.strictEqual(dObj.foo, dObj.bar[0]); 53 | assert.strictEqual(dObj.foo, dObj.bar[1]); 54 | }); 55 | 56 | it("should work with Buffer objects", function () { 57 | var b = new Buffer("asdf"); 58 | var bb = arson.decode(arson.encode([b, b])); 59 | assert.strictEqual(bb[0], bb[1]); 60 | assert.ok(bb[0] instanceof Buffer); 61 | assert.strictEqual(bb[0].toString("utf8"), "asdf"); 62 | }); 63 | 64 | typeof Map === "function" && 65 | typeof Array.from === "function" && 66 | it("should work with Map objects", function () { 67 | var m1 = new Map; 68 | var value = { foo: 42 }; 69 | m1.set(1234, value); 70 | m1.set(value, m1); 71 | m1.set(m1, "self"); 72 | assert.strictEqual(m1.get(m1.get(1234)), m1); 73 | var m2 = arson.decode(arson.encode(m1)); 74 | assert.strictEqual(m2.get(m2.get(1234)), m2); 75 | assert.strictEqual(m2.get(m2), "self"); 76 | }); 77 | 78 | typeof Set === "function" && 79 | typeof Array.from === "function" && 80 | it("should work with Set objects", function () { 81 | var s1 = new Set; 82 | s1.add(s1); 83 | 84 | var s2 = arson.decode(arson.encode(s1)); 85 | assert.strictEqual(Array.from(s2)[0], s2); 86 | s2.add(s1); 87 | 88 | var s3 = arson.decode(arson.encode(s2)); 89 | var elems = Array.from(s3); 90 | 91 | assert.strictEqual(elems.length, 2); 92 | assert.notStrictEqual(elems[0], elems[1]); 93 | elems.forEach(function (s) { 94 | assert.ok(s.has(s)); 95 | }); 96 | }); 97 | 98 | it("should work for sparse arrays", function () { 99 | function check(array) { 100 | assert.deepEqual(array, arson.decode(arson.encode(array))); 101 | } 102 | 103 | check([,]); 104 | check([,,]); 105 | check([,,,]); 106 | check([1,,3]); 107 | check([1,,3,,4]); 108 | check([1,,3,,4,,]); 109 | }); 110 | 111 | it("should work with circular references", function () { 112 | var obj = {}; 113 | obj.self = obj; 114 | var result = arson.decode(arson.encode(obj)); 115 | assert.notStrictEqual(result, obj); 116 | assert.strictEqual(result.self, result); 117 | }); 118 | 119 | it("should work with repeated references", function () { 120 | var a = {}; 121 | var b = { foo: 42 }; 122 | a.x = a.y = b; 123 | var result = arson.decode(arson.encode(a)); 124 | assert.strictEqual(result.x, result.y); 125 | }); 126 | 127 | it("should work with the global object", function () { 128 | var copy = arson.decode(arson.encode(global)); 129 | assert.strictEqual(copy.global, copy); 130 | }); 131 | 132 | it("should preserve identity of immutable tuples", function () { 133 | var tuple = require("immutable-tuple").tuple; 134 | 135 | arson.registerType("tuple", { 136 | deconstruct: function (t) { 137 | if (tuple.isTuple(t)) { 138 | return Array.from(t); 139 | } 140 | }, 141 | 142 | reconstruct: function (entries) { 143 | if (entries) { 144 | return tuple.apply(null, entries); 145 | } 146 | } 147 | }); 148 | 149 | var t1 = tuple(1, 2, tuple(3, 4), 5); 150 | var input = ["asdf", t1, true]; 151 | var output = arson.decode(arson.encode(input)); 152 | 153 | assert.notStrictEqual(input, output); 154 | assert.strictEqual(output[1], t1); 155 | }); 156 | }); 157 | --------------------------------------------------------------------------------