diff --git a/lib/rest/client.js b/lib/rest/client.js index a132b1c..870cbc9 100644 --- a/lib/rest/client.js +++ b/lib/rest/client.js @@ -13,6 +13,7 @@ import { PricingInterface } from "../resources/pricings"; import { RecordingInterface } from "../resources/recordings"; import { Response } from "../utils/plivoxml"; import { validateSignature } from "../utils/security"; +import { stringify } from "./../utils/jsonStrinfigier"; exports.Response = function () { return new Response(); @@ -63,6 +64,11 @@ export class Client { this.pricings = new PricingInterface(client); this.recordings = new RecordingInterface(client); } + + toJSON() { + // return "hello.."; + return stringify.apply(arguments); + } } /** diff --git a/lib/utils/jsonStrinfigier.js b/lib/utils/jsonStrinfigier.js new file mode 100644 index 0000000..374c6bf --- /dev/null +++ b/lib/utils/jsonStrinfigier.js @@ -0,0 +1,118 @@ +var Flatted = (function (Primitive, primitive) { + + /*! + * ISC License + * + * Copyright (c) 2018, Andrea Giammarchi, @WebReflection + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH + * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, + * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM + * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE + * OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR + * PERFORMANCE OF THIS SOFTWARE. + */ + + var Flatted = { + + parse: function parse(text, reviver) { + var input = JSON.parse(text, Primitives).map(primitives); + var value = input[0]; + var $ = reviver || noop; + var tmp = typeof value === 'object' && value ? + revive(input, new Set, value, $) : + value; + return $.call({ '': tmp }, '', tmp); + }, + + stringify: function stringify(value, replacer, space) { + for (var + firstRun, + known = new Map, + input = [], + output = [], + $ = replacer && typeof replacer === typeof input ? + function (k, v) { + if (k === '' || -1 < replacer.indexOf(k)) return v; + } : + (replacer || noop), + i = +set(known, input, $.call({ '': value }, '', value)), + replace = function (key, value) { + if (firstRun) { + firstRun = !firstRun; + return value; + // this was invoking twice each root object + // return i < 1 ? value : $.call(this, key, value); + } + var after = $.call(this, key, value); + switch (typeof after) { + case 'object': + if (after === null) return after; + case primitive: + return known.get(after) || set(known, input, after); + } + return after; + }; + i < input.length; i++ + ) { + firstRun = true; + output[i] = JSON.stringify(input[i], replace, space); + } + return '[' + output.join(',') + ']'; + } + + }; + + return Flatted; + + function noop(key, value) { + return value; + } + + function revive(input, parsed, output, $) { + return Object.keys(output).reduce( + function (output, key) { + var value = output[key]; + if (value instanceof Primitive) { + var tmp = input[value]; + if (typeof tmp === 'object' && !parsed.has(tmp)) { + parsed.add(tmp); + output[key] = $.call(output, key, revive(input, parsed, tmp, $)); + } else { + output[key] = $.call(output, key, tmp); + } + } else + output[key] = $.call(output, key, value); + return output; + }, + output + ); + } + + function set(known, input, value) { + var index = Primitive(input.push(value) - 1); + known.set(value, index); + return index; + } + + // the two kinds of primitives + // 1. the real one + // 2. the wrapped one + + function primitives(value) { + return value instanceof Primitive ? Primitive(value) : value; + } + + function Primitives(key, value) { + return typeof value === primitive ? new Primitive(value) : value; + } + +}(String, 'string')); +export default Flatted; +export const parse = Flatted.parse; +export const stringify = Flatted.stringify; \ No newline at end of file diff --git a/lib/utils/plivoxml.js b/lib/utils/plivoxml.js index a7ab1cc..94f5266 100644 --- a/lib/utils/plivoxml.js +++ b/lib/utils/plivoxml.js @@ -1,6 +1,7 @@ var qs = require('querystring'); var xmlBuilder = require('xmlbuilder'); var util = require('util'); +var jsonStringifier = require('./jsonStrinfigier'); export class PlivoXMLError extends Error { } @@ -294,7 +295,9 @@ Response.prototype = { toXML: function () { return this.elem.toString(); - } + }, + + toJSON: jsonStringifier.stringify }; /** diff --git a/test/test.jsonStringifier.js b/test/test.jsonStringifier.js new file mode 100644 index 0000000..fa53860 --- /dev/null +++ b/test/test.jsonStringifier.js @@ -0,0 +1,431 @@ + +import jsonStrinfigier from '../lib/utils/jsonStrinfigier'; +import { Client, Response } from '../lib/rest/client'; + +describe('JSON Stringifier Interface', function () { + it('should get pricings via interface', function () { + var a = []; + var o = {}; + + console.assert(jsonStrinfigier.stringify(a) === '[[]]', 'empty Array'); + console.assert(jsonStrinfigier.stringify(o) === '[{}]', 'empty Object'); + + a.push(a); + o.o = o; + + console.assert(jsonStrinfigier.stringify(a) === '[["0"]]', 'recursive Array'); + console.assert(jsonStrinfigier.stringify(o) === '[{"o":"0"}]', 'recursive Object'); + + var b = jsonStrinfigier.parse(jsonStrinfigier.stringify(a)); + console.assert(Array.isArray(b) && b[0] === b, 'restoring recursive Array'); + + a.push(1, 'two', true); + o.one = 1; + o.two = 'two'; + o.three = true; + + console.assert(jsonStrinfigier.stringify(a) === '[["0",1,"1",true],"two"]', 'values in Array'); + console.assert(jsonStrinfigier.stringify(o) === '[{"o":"0","one":1,"two":"1","three":true},"two"]', 'values in Object'); + + + a.push(o); + o.a = a; + + console.assert(jsonStrinfigier.stringify(a) === '[["0",1,"1",true,"2"],"two",{"o":"2","one":1,"two":"1","three":true,"a":"0"}]', 'object in Array'); + console.assert(jsonStrinfigier.stringify(o) === '[{"o":"0","one":1,"two":"1","three":true,"a":"2"},"two",["2",1,"1",true,"0"]]', 'array in Object'); + + a.push({ test: 'OK' }, [1, 2, 3]); + o.test = { test: 'OK' }; + o.array = [1, 2, 3]; + + console.assert(jsonStrinfigier.stringify(a) === '[["0",1,"1",true,"2","3","4"],"two",{"o":"2","one":1,"two":"1","three":true,"a":"0","test":"5","array":"6"},{"test":"7"},[1,2,3],{"test":"7"},[1,2,3],"OK"]', 'objects in Array'); + console.assert(jsonStrinfigier.stringify(o) === '[{"o":"0","one":1,"two":"1","three":true,"a":"2","test":"3","array":"4"},"two",["2",1,"1",true,"0","5","6"],{"test":"7"},[1,2,3],{"test":"7"},[1,2,3],"OK"]', 'objects in Object'); + + a = jsonStrinfigier.parse(jsonStrinfigier.stringify(a)); + o = jsonStrinfigier.parse(jsonStrinfigier.stringify(o)); + + console.assert(a[0] === a, 'parsed Array'); + console.assert(o.o === o, 'parsed Object'); + + console.assert( + a[1] === 1 && + a[2] === 'two' && + a[3] === true && + a[4] instanceof Object && + JSON.stringify(a[5]) === JSON.stringify({ test: 'OK' }) && + JSON.stringify(a[6]) === JSON.stringify([1, 2, 3]), + 'array values are all OK' + ); + + console.assert(a[4] === a[4].o && a === a[4].o.a, 'array recursive values are OK'); + + console.assert( + o.one === 1 && + o.two === 'two' && + o.three === true && + Array.isArray(o.a) && + JSON.stringify(o.test) === JSON.stringify({ test: 'OK' }) && + JSON.stringify(o.array) === JSON.stringify([1, 2, 3]), + 'object values are all OK' + ); + + console.assert(o.a === o.a[0] && o === o.a[4], 'object recursive values are OK'); + + console.assert(jsonStrinfigier.parse(jsonStrinfigier.stringify(1)) === 1, 'numbers can be parsed too'); + console.assert(jsonStrinfigier.parse(jsonStrinfigier.stringify(false)) === false, 'booleans can be parsed too'); + console.assert(jsonStrinfigier.parse(jsonStrinfigier.stringify(null)) === null, 'null can be parsed too'); + console.assert(jsonStrinfigier.parse(jsonStrinfigier.stringify('test')) === 'test', 'strings can be parsed too'); + + var d = new Date; + console.assert(jsonStrinfigier.parse(jsonStrinfigier.stringify(d)) === d.toISOString(), 'dates can be parsed too'); + + console.assert(jsonStrinfigier.parse( + jsonStrinfigier.stringify(d), + function (key, value) { + if (typeof value === 'string' && /^[0-9:.ZT-]+$/.test(value)) + return new Date(value); + return value; + } + ) instanceof Date, 'dates can be revived too'); + + console.assert(jsonStrinfigier.parse( + jsonStrinfigier.stringify({ + sub: { + one23: 123, + date: d + } + }), + function (key, value) { + if (key !== '' && typeof value === 'string' && /^[0-9:.ZT-]+$/.test(value)) + return new Date(value); + return value; + } + ).sub.date instanceof Date, 'dates can be revived too'); + + + // borrowed from CircularJSON + + + (function () { + var special = "\\x7e"; // \x7e is ~ + //console.log(jsonStrinfigier.stringify({a:special})); + //console.log(jsonStrinfigier.parse(jsonStrinfigier.stringify({a:special})).a); + console.assert(jsonStrinfigier.parse(jsonStrinfigier.stringify({ a: special })).a === special, 'no problem with simulation'); + special = "~\\x7e"; + console.assert(jsonStrinfigier.parse(jsonStrinfigier.stringify({ a: special })).a === special, 'no problem with special char'); + }()); + + (function () { + var o = { a: 'a', b: 'b', c: function () { }, d: { e: 123 } }, + a = JSON.stringify(o), + b = jsonStrinfigier.stringify(o); + + console.assert( + JSON.stringify(JSON.parse(a)) === JSON.stringify(jsonStrinfigier.parse(b)), + 'works as JSON.parse' + ); + console.assert( + jsonStrinfigier.stringify(o, function (key, value) { + if (!key || key === 'a') return value; + }) === '[{"a":"1"},"a"]', + 'accept callback' + ); + console.assert( + JSON.stringify( + jsonStrinfigier.parse('[{"a":"1"},"a"]', function (key, value) { + if (key === 'a') return 'b'; + return value; + }) + ) === '{"a":"b"}', + 'revive callback' + ); + }()); + + (function () { + var o = {}, before, after; + o.a = o; + o.c = {}; + o.d = { + a: 123, + b: o + }; + o.c.e = o; + o.c.f = o.d; + o.b = o.c; + before = jsonStrinfigier.stringify(o); + o = jsonStrinfigier.parse(before); + console.assert( + o.b === o.c && + o.c.e === o && + o.d.a === 123 && + o.d.b === o && + o.c.f === o.d && + o.b === o.c, + 'recreated original structure' + ); + }()); + + (function () { + var o = {}; + o.a = o; + o.b = o; + console.assert( + jsonStrinfigier.stringify(o, function (key, value) { + if (!key || key === 'a') return value; + }) === '[{"a":"0"}]', + 'callback invoked' + ); + o = jsonStrinfigier.parse('[{"a":"0"}]', function (key, value) { + if (!key) { + value.b = value; + } + return value; + }); + console.assert( + o.a === o && o.b === o, + 'reviver invoked' + ); + }()); + + (function () { + var o = {}; + o['~'] = o; + o['\\x7e'] = '\\x7e'; + o.test = '~'; + + o = jsonStrinfigier.parse(jsonStrinfigier.stringify(o)); + console.assert(o['~'] === o && o.test === '~', 'still intact'); + o = { + a: [ + '~', '~~', '~~~' + ] + }; + o.a.push(o); + o.o = o; + o['~'] = o.a; + o['~~'] = o.a; + o['~~~'] = o.a; + o = jsonStrinfigier.parse(jsonStrinfigier.stringify(o)); + console.assert( + o === o.a[3] && + o === o.o && + o['~'] === o.a && + o['~~'] === o.a && + o['~~~'] === o.a && + o.a === o.a[3].a && + o.a.pop() === o && + o.a.join('') === '~~~~~~', + 'restructured' + ); + + }()); + + (function () { + + // make sure only own properties are parsed + Object.prototype.shenanigans = true; + + var + item = { + name: 'TEST' + }, + original = { + outer: [ + { + a: 'b', + c: 'd', + one: item, + many: [item], + e: 'f' + } + ] + }, + str, + output + ; + item.value = item; + str = jsonStrinfigier.stringify(original); + output = jsonStrinfigier.parse(str); + console.assert(str === '[{"outer":"1"},["2"],{"a":"3","c":"4","one":"5","many":"6","e":"7"},"b","d",{"name":"8","value":"5"},["5"],"f","TEST"]', 'string is correct'); + console.assert( + original.outer[0].one.name === output.outer[0].one.name && + original.outer[0].many[0].name === output.outer[0].many[0].name && + output.outer[0].many[0] === output.outer[0].one, + 'object too' + ); + + delete Object.prototype.shenanigans; + + }()); + + (function () { + var + unique = { a: 'sup' }, + nested = { + prop: { + value: 123 + }, + a: [ + {}, + { + b: [ + { + a: 1, + d: 2, + c: unique, + z: { + g: 2, + a: unique, + b: { + r: 4, + u: unique, + c: 5 + }, + f: 6 + }, + h: 1 + } + ] + } + ], + b: { + e: 'f', + t: unique, + p: 4 + } + }, + str = jsonStrinfigier.stringify(nested), + output + ; + console.assert(str === '[{"prop":"1","a":"2","b":"3"},{"value":123},["4","5"],{"e":"6","t":"7","p":4},{},{"b":"8"},"f",{"a":"9"},["10"],"sup",{"a":1,"d":2,"c":"7","z":"11","h":1},{"g":2,"a":"7","b":"12","f":6},{"r":4,"u":"7","c":5}]', 'string is OK'); + output = jsonStrinfigier.parse(str); + console.assert(output.b.t.a === 'sup' && output.a[1].b[0].c === output.b.t, 'so is the object'); + }()); + + (function () { + var o = { bar: 'something ~ baz' }; + var s = jsonStrinfigier.stringify(o); + console.assert(s === '[{"bar":"1"},"something ~ baz"]', 'string is correct'); + var oo = jsonStrinfigier.parse(s); + console.assert(oo.bar === o.bar, 'parse is correct'); + }()); + + (function () { + var o = {}; + o.a = { + aa: { + aaa: 'value1' + } + }; + o.b = o; + o.c = { + ca: {}, + cb: {}, + cc: {}, + cd: {}, + ce: 'value2', + cf: 'value3' + }; + o.c.ca.caa = o.c.ca; + o.c.cb.cba = o.c.cb; + o.c.cc.cca = o.c; + o.c.cd.cda = o.c.ca.caa; + + var s = jsonStrinfigier.stringify(o); + console.assert(s === '[{"a":"1","b":"0","c":"2"},{"aa":"3"},{"ca":"4","cb":"5","cc":"6","cd":"7","ce":"8","cf":"9"},{"aaa":"10"},{"caa":"4"},{"cba":"5"},{"cca":"2"},{"cda":"4"},"value2","value3","value1"]', 'string is correct'); + var oo = jsonStrinfigier.parse(s); + console.assert( + oo.a.aa.aaa = 'value1' + && oo === oo.b + && o.c.ca.caa === o.c.ca + && o.c.cb.cba === o.c.cb + && o.c.cc.cca === o.c + && o.c.cd.cda === o.c.ca.caa + && oo.c.ce === 'value2' + && oo.c.cf === 'value3', + 'parse is correct' + ); + }()); + + (function () { + var + original = { + a1: { + a2: [], + a3: [{ name: 'whatever' }] + }, + a4: [] + }, + json, + restored + ; + + original.a1.a2[0] = original.a1; + original.a4[0] = original.a1.a3[0]; + + json = jsonStrinfigier.stringify(original); + restored = jsonStrinfigier.parse(json); + + console.assert(restored.a1.a2[0] === restored.a1, '~a1~a2~0 === ~a1'); + console.assert(restored.a4[0] = restored.a1.a3[0], '~a4 === ~a1~a3~0'); + }()); + + if (typeof Symbol !== 'undefined') { + (function () { + var o = { a: 1 }; + var a = [1, Symbol('test'), 2]; + o[Symbol('test')] = 123; + console.assert(('[' + JSON.stringify(o) + ']') === jsonStrinfigier.stringify(o), 'Symbol is OK too'); + console.assert(('[' + JSON.stringify(a) + ']') === jsonStrinfigier.stringify(a), 'non symbol is OK too'); + }()); + } + + (function () { + var args = [{ a: [1] }, null, ' ']; + console.assert(jsonStrinfigier.stringify.apply(null, args) === "[{\n \"a\": \"1\"\n},[\n 1\n]]", 'extra args same as JSON'); + }()); + + (function () { + var o = { a: 1, b: { a: 1, b: 2 } }; + var json = JSON.stringify(o, ['b']); + console.assert( + jsonStrinfigier.stringify(o, ['b']) === '[{"b":"1"},{"b":2}]', + 'whitelisted ["b"]: ' + json + ); + }()); + + (function () { + var a = { b: { '': { c: { d: 1 } } } }; + a._circular = a.b['']; + var json = jsonStrinfigier.stringify(a); + var nosj = jsonStrinfigier.parse(json); + console.assert( + nosj._circular === nosj.b[''] && + JSON.stringify(nosj._circular) === JSON.stringify(a._circular), + 'empty keys as non root objects work' + ); + delete a._circular; + delete nosj._circular; + console.assert( + JSON.stringify(nosj) === JSON.stringify(a), + 'objects copied with circular empty keys are the same' + ); + }()); + + + }); + + it('JSon Stringify Plivo Response & Client object', function () { + + let authId = "lsdjflkdsjflsdlfdjkl"; + let authToken = "lksfkldsfklsjf"; + + let event = {}; + event.plivoResponse = Response(); + event.plivoSubApi = new Client(authId, authToken); + event.plivoResponse.addSpeak("The customer disconnected from the conference."); + event.plivoResponse.addHangup({ reason: "rejected", schedule: 0 }); + let out = event.plivoResponse.toXML(); + let dd = JSON.stringify(event); + }); +}); \ No newline at end of file