stringify.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498
  1. // tests stringify()
  2. /*global require console exports */
  3. // set to true to show performance stats
  4. var DEBUG = false;
  5. var assert = require('assert');
  6. var JSON5 = require('../lib/json5');
  7. // Test JSON5.stringify() by comparing its output for each case with
  8. // native JSON.stringify(). The only differences will be in how object keys are
  9. // handled.
  10. var simpleCases = [
  11. null,
  12. 9, -9, +9, +9.878,
  13. '', "''", '999', '9aa', 'aaa', 'aa a', 'aa\na', 'aa\\a', '\'', '\\\'', '\\"',
  14. undefined,
  15. true, false,
  16. {}, [], function(){},
  17. Date.now(), new Date(Date.now())
  18. ];
  19. exports.stringify = {};
  20. exports.stringify.simple = function test() {
  21. for (var i=0; i<simpleCases.length; i++) {
  22. assertStringify(simpleCases[i]);
  23. }
  24. };
  25. exports.stringify.oddities = function test() {
  26. assertStringify(Function);
  27. assertStringify(Date);
  28. assertStringify(Object);
  29. assertStringify(NaN);
  30. assertStringify(Infinity);
  31. assertStringify(10e6);
  32. assertStringify(19.3223e6);
  33. assertStringify(077);
  34. assertStringify(0x99);
  35. assertStringify(/aa/);
  36. assertStringify(new RegExp('aa'));
  37. assertStringify(new Number(7));
  38. assertStringify(new String(7));
  39. assertStringify(new String(""));
  40. assertStringify(new String("abcde"));
  41. assertStringify(new String(new String("abcde")));
  42. assertStringify(new Boolean(true));
  43. assertStringify(new Boolean());
  44. };
  45. exports.stringify.arrays = function test() {
  46. assertStringify([""]);
  47. assertStringify([1, 2]);
  48. assertStringify([undefined]);
  49. assertStringify([1, 'fasds']);
  50. assertStringify([1, '\n\b\t\f\r\'']);
  51. assertStringify([1, 'fasds', ['fdsafsd'], null]);
  52. assertStringify([1, 'fasds', ['fdsafsd'], null, function(aaa) { return 1; }, false ]);
  53. assertStringify([1, 'fasds', ['fdsafsd'], undefined, function(aaa) { return 1; }, false ]);
  54. };
  55. exports.stringify.objects = function test() {
  56. assertStringify({a:1, b:2});
  57. assertStringify({"":1, b:2});
  58. assertStringify({9:1, b:2});
  59. assertStringify({"9aaa":1, b:2});
  60. assertStringify({aaaa:1, bbbb:2});
  61. assertStringify({a$a_aa:1, bbbb:2});
  62. assertStringify({"a$a_aa":1, 'bbbb':2});
  63. assertStringify({"a$a_aa":[1], 'bbbb':{a:2}});
  64. assertStringify({"a$22222_aa":[1], 'bbbb':{aaaa:2, name:function(a,n,fh,h) { return 'nuthin'; }, foo: undefined}});
  65. assertStringify({"a$222222_aa":[1], 'bbbb':{aaaa:2, name:'other', foo: undefined}});
  66. assertStringify({"a$222222_aa":[1, {}, undefined, function() { }, { jjj: function() { } }], 'bbbb':{aaaa:2, name:'other', foo: undefined}});
  67. // using same obj multiple times
  68. var innerObj = {a: 9, b:6};
  69. assertStringify({a : innerObj, b: innerObj, c: [innerObj, innerObj, innerObj]});
  70. };
  71. exports.stringify.oddKeys = function test() {
  72. assertStringify({"this is a crazy long key":1, 'bbbb':2});
  73. assertStringify({"":1, 'bbbb':2});
  74. assertStringify({"s\ns":1, 'bbbb':2});
  75. assertStringify({'\n\b\t\f\r\'\\':1, 'bbbb':2});
  76. assertStringify({undefined:1, 'bbbb':2});
  77. assertStringify({'\x00':'\x00'});
  78. };
  79. // we expect errors from all of these tests. The errors should match
  80. exports.stringify.circular = function test() {
  81. var obj = { };
  82. obj.obj = obj;
  83. assertStringify(obj, null, true);
  84. var obj2 = {inner1: {inner2: {}}};
  85. obj2.inner1.inner2.obj = obj2;
  86. assertStringify(obj2, null, true);
  87. var obj3 = {inner1: {inner2: []}};
  88. obj3.inner1.inner2[0] = obj3;
  89. assertStringify(obj3, null, true);
  90. };
  91. exports.stringify.replacerType = function test() {
  92. var assertStringifyJSON5ThrowsExceptionForReplacer = function(replacer) {
  93. assert.throws(
  94. function() { JSON5.stringify(null, replacer); },
  95. /Replacer must be a function or an array/
  96. );
  97. };
  98. assertStringifyJSON5ThrowsExceptionForReplacer('string');
  99. assertStringifyJSON5ThrowsExceptionForReplacer(123);
  100. assertStringifyJSON5ThrowsExceptionForReplacer({});
  101. };
  102. exports.stringify.replacer = {};
  103. exports.stringify.replacer.function = {};
  104. exports.stringify.replacer.function.simple = function test() {
  105. function replacerTestFactory(expectedValue) {
  106. return function() {
  107. var lastKey = null,
  108. lastValue = null,
  109. numCalls = 0,
  110. replacerThis;
  111. return {
  112. replacer: function(key, value) {
  113. lastKey = key;
  114. lastValue = value;
  115. numCalls++;
  116. replacerThis = this;
  117. return value;
  118. },
  119. assert: function() {
  120. assert.equal(numCalls, 1, "Replacer should be called exactly once for " + expectedValue);
  121. assert.equal(lastKey, "");
  122. assert.deepEqual(replacerThis, {"":expectedValue});
  123. var expectedValueToJson = expectedValue;
  124. if (expectedValue && expectedValue['toJSON']) {
  125. expectedValueToJson = expectedValue.toJSON();
  126. }
  127. assert.equal(lastValue, expectedValueToJson);
  128. }
  129. }
  130. }
  131. }
  132. for (var i=0; i<simpleCases.length; i++) {
  133. assertStringify(simpleCases[i], replacerTestFactory(simpleCases[i]));
  134. }
  135. };
  136. exports.stringify.replacer.function.complexObject = function test() {
  137. var obj = {
  138. "": "emptyPropertyName",
  139. one: 'string',
  140. two: 123,
  141. three: ['array1', 'array2'],
  142. four: {nested_one:'anotherString'},
  143. five: new Date(),
  144. six: Date.now(),
  145. seven: null,
  146. eight: true,
  147. nine: false,
  148. ten: [NaN, Infinity, -Infinity],
  149. eleven: function() {}
  150. };
  151. var expectedKeys = [
  152. '', // top level object
  153. '', // First key
  154. 'one',
  155. 'two',
  156. 'three', 0, 1, // array keys
  157. 'four', 'nested_one', // nested object keys
  158. 'five',
  159. 'six',
  160. 'seven',
  161. 'eight',
  162. 'nine',
  163. 'ten', 0, 1, 2, // array keys
  164. 'eleven'
  165. ];
  166. var expectedHolders = [
  167. {"": obj},
  168. obj,
  169. obj,
  170. obj,
  171. obj, obj.three, obj.three,
  172. obj, obj.four,
  173. obj,
  174. obj,
  175. obj,
  176. obj,
  177. obj,
  178. obj, obj.ten, obj.ten, obj.ten,
  179. obj
  180. ];
  181. var ReplacerTest = function() {
  182. var seenKeys = [];
  183. var seenHolders = [];
  184. return {
  185. replacer: function(key, value) {
  186. seenKeys.push(key);
  187. seenHolders.push(this);
  188. if (typeof(value) == "object") {
  189. return value;
  190. }
  191. return 'replaced ' + (value ? value.toString() : '');
  192. },
  193. assert: function() {
  194. assert.deepEqual(seenKeys, expectedKeys);
  195. assert.deepEqual(seenHolders, expectedHolders);
  196. }
  197. }
  198. };
  199. assertStringify(obj, ReplacerTest);
  200. };
  201. exports.stringify.replacer.function.replacingWithUndefined = function test() {
  202. var obj = { shouldSurvive: 'one', shouldBeRemoved: 'two' };
  203. var ReplacerTest = function() {
  204. return {
  205. replacer: function(key, value) {
  206. if (key === 'shouldBeRemoved') {
  207. return undefined;
  208. } else {
  209. return value;
  210. }
  211. },
  212. assert: function() { /* no-op */ }
  213. }
  214. };
  215. assertStringify(obj, ReplacerTest);
  216. };
  217. exports.stringify.replacer.function.replacingArrayValueWithUndefined = function test() {
  218. var obj = ['should survive', 'should be removed'];
  219. var ReplacerTest = function() {
  220. return {
  221. replacer: function(key, value) {
  222. if (value === 'should be removed') {
  223. return undefined;
  224. } else {
  225. return value;
  226. }
  227. },
  228. assert: function() { /* no-op */ }
  229. }
  230. };
  231. assertStringify(obj, ReplacerTest);
  232. };
  233. exports.stringify.replacer.array = {};
  234. exports.stringify.replacer.array.simple = function test() {
  235. var ReplacerTest = function() {
  236. return {
  237. replacer: [],
  238. assert: function() { /* no-op */ }
  239. }
  240. };
  241. for (var i=0; i<simpleCases.length; i++) {
  242. assertStringify(simpleCases[i], ReplacerTest);
  243. }
  244. };
  245. exports.stringify.replacer.array.emptyStringProperty = function test() {
  246. var obj = {'': 'keep', 'one': 'remove'};
  247. var ReplacerTest = function() {
  248. return {
  249. replacer: [''],
  250. assert: function() {/* no-op */}
  251. }
  252. };
  253. assertStringify(obj, ReplacerTest);
  254. };
  255. exports.stringify.replacer.array.complexObject = function test() {
  256. var obj = {
  257. "": "emptyPropertyName",
  258. one: 'string',
  259. one_remove: 'string',
  260. two: 123,
  261. two_remove: 123,
  262. three: ['array1', 'array2'],
  263. three_remove: ['array1', 'array2'],
  264. four: {nested_one:'anotherString', nested_one_remove: 'anotherString'},
  265. four_remove: {nested_one:'anotherString', nested_one_remove: 'anotherString'},
  266. five: new Date(),
  267. five_remove: new Date(),
  268. six: Date.now(),
  269. six_remove: Date.now(),
  270. seven: null,
  271. seven_remove: null,
  272. eight: true,
  273. eight_remove: true,
  274. nine: false,
  275. nine_remove: false,
  276. ten: [NaN, Infinity, -Infinity],
  277. ten_remove: [NaN, Infinity, -Infinity],
  278. eleven: function() {},
  279. eleven_remove: function() {}
  280. };
  281. var ReplacerTest = function() {
  282. return {
  283. replacer: [
  284. 'one', 'two', 'three', 'four', 'nested_one', 'five', 'six', 'seven', 'eight', 'nine', 'ten', 'eleven', 0
  285. ],
  286. assert: function() {/* no-op */}
  287. }
  288. };
  289. assertStringify(obj, ReplacerTest);
  290. };
  291. exports.stringify.toJSON = function test() {
  292. var customToJSONObject = {
  293. name: 'customToJSONObject',
  294. toJSON: function() {
  295. return 'custom-to-json-object-serialization';
  296. }
  297. };
  298. assertStringify(customToJSONObject);
  299. var customToJSONPrimitive = "Some string";
  300. customToJSONPrimitive.toJSON = function() {
  301. return 'custom-to-json-string-serialization';
  302. };
  303. assertStringify(customToJSONPrimitive);
  304. var object = {
  305. customToJSONObject: customToJSONObject
  306. };
  307. assertStringify(object);
  308. // Returning an object with a toJSON function does *NOT* have that toJSON function called: it is omitted
  309. var nested = {
  310. name: 'nested',
  311. toJSON: function() {
  312. return customToJSONObject;
  313. }
  314. };
  315. assertStringify(nested);
  316. var count = 0;
  317. function createObjectSerialisingTo(value) {
  318. count++;
  319. return {
  320. name: 'obj-' + count,
  321. toJSON: function() {
  322. return value;
  323. }
  324. };
  325. }
  326. assertStringify(createObjectSerialisingTo(null));
  327. assertStringify(createObjectSerialisingTo(undefined));
  328. assertStringify(createObjectSerialisingTo([]));
  329. assertStringify(createObjectSerialisingTo({}));
  330. assertStringify(createObjectSerialisingTo(12345));
  331. assertStringify(createObjectSerialisingTo(true));
  332. assertStringify(createObjectSerialisingTo(new Date()));
  333. assertStringify(createObjectSerialisingTo(function(){}));
  334. };
  335. function stringifyJSON5(obj, replacer, space) {
  336. var start, res, end;
  337. try {
  338. start = new Date();
  339. res = JSON5.stringify(obj, replacer, space);
  340. end = new Date();
  341. } catch (e) {
  342. res = e.message;
  343. end = new Date();
  344. }
  345. if (DEBUG) {
  346. console.log('JSON5.stringify time: ' + (end-start));
  347. console.log(res);
  348. }
  349. return res;
  350. }
  351. function stringifyJSON(obj, replacer, space) {
  352. var start, res, end;
  353. try {
  354. start = new Date();
  355. res = JSON.stringify(obj, replacer, space);
  356. end = new Date();
  357. // now remove all quotes from keys where appropriate
  358. // first recursively find all key names
  359. var keys = [];
  360. function findKeys(key, innerObj) {
  361. if (innerObj && innerObj.toJSON && typeof innerObj.toJSON === "function") {
  362. innerObj = innerObj.toJSON();
  363. }
  364. if (replacer) {
  365. if (typeof replacer === 'function') {
  366. innerObj = replacer(key, innerObj);
  367. } else if (key !== '' && replacer.indexOf(key) < 0) {
  368. return;
  369. }
  370. }
  371. if (JSON5.isWord(key) &&
  372. typeof innerObj !== 'function' &&
  373. typeof innerObj !== 'undefined') {
  374. keys.push(key);
  375. }
  376. if (typeof innerObj === 'object') {
  377. if (Array.isArray(innerObj)) {
  378. for (var i = 0; i < innerObj.length; i++) {
  379. findKeys(i, innerObj[i]);
  380. }
  381. } else if (innerObj !== null) {
  382. for (var prop in innerObj) {
  383. if (innerObj.hasOwnProperty(prop)) {
  384. findKeys(prop, innerObj[prop]);
  385. }
  386. }
  387. }
  388. }
  389. }
  390. findKeys('', obj);
  391. // now replace each key in the result
  392. var last = 0;
  393. for (var i = 0; i < keys.length; i++) {
  394. // not perfect since we can match on parts of the previous value that
  395. // matches the key, but we can design our test around that.
  396. last = res.indexOf('"' + keys[i] + '"', last);
  397. if (last === -1) {
  398. // problem with test framework
  399. console.log("Couldn't find: " + keys[i]);
  400. throw new Error("Couldn't find: " + keys[i]);
  401. }
  402. res = res.substring(0, last) +
  403. res.substring(last+1, last + keys[i].length+1) +
  404. res.substring(last + keys[i].length + 2, res.length);
  405. last += keys[i].length;
  406. }
  407. } catch (e) {
  408. res = e.message;
  409. end = new Date();
  410. }
  411. if (DEBUG) {
  412. console.log('JSON.stringify time: ' + (end-start));
  413. }
  414. return res;
  415. }
  416. function assertStringify(obj, replacerTestConstructor, expectError) {
  417. if (!replacerTestConstructor) {
  418. replacerTestConstructor = function(){
  419. return {replacer: null, assert: function(){}};
  420. };
  421. }
  422. var testStringsEqual = function(obj, indent) {
  423. var j5ReplacerTest = replacerTestConstructor();
  424. var jReplacerTest = replacerTestConstructor();
  425. var j5, j;
  426. j5 = stringifyJSON5(obj, j5ReplacerTest.replacer, indent);
  427. j = stringifyJSON(obj, jReplacerTest.replacer, indent);
  428. assert.equal(j5, j);
  429. j5ReplacerTest.assert();
  430. };
  431. var indents = [
  432. undefined,
  433. " ",
  434. " ",
  435. " ",
  436. "\t",
  437. "this is an odd indent",
  438. 5,
  439. 20,
  440. '\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t'
  441. ];
  442. for (var i=0; i<indents.length; i++) {
  443. testStringsEqual(obj, indents[i]);
  444. }
  445. if (!expectError) {
  446. // no point in round tripping if there is an error
  447. var origStr = JSON5.stringify(obj), roundTripStr;
  448. if (origStr !== "undefined" && typeof origStr !== "undefined") {
  449. try {
  450. roundTripStr = JSON5.stringify(JSON5.parse(origStr));
  451. } catch (e) {
  452. console.log(e);
  453. console.log(origStr);
  454. throw e;
  455. }
  456. assert.equal(origStr, roundTripStr);
  457. }
  458. }
  459. }