diff --git a/jstp.js b/jstp.js index 5daa938..b74b09d 100644 --- a/jstp.js +++ b/jstp.js @@ -5,7 +5,6 @@ module.exports = jstp; Object.assign(jstp, require('./lib/record-serialization'), - require('./lib/object-serialization'), require('./lib/errors'), require('./lib/applications') ); diff --git a/lib/json5-serialize.js b/lib/json5-serialize.js new file mode 100644 index 0000000..dd6110c --- /dev/null +++ b/lib/json5-serialize.js @@ -0,0 +1,84 @@ +'use strict'; + +module.exports = serialize; + +function serialize(object) { + let type; + if (Array.isArray(object)) { + type = 'array'; + } else if (object instanceof Date) { + type = 'date'; + } else if (object === null) { + type = 'null'; + } else { + type = typeof(object); + } + + const serializer = serialize.types[type]; + if (serializer) { + return serializer(object); + } + + return ''; +} + +serialize.types = { + number: number => number + '', + boolean: boolean => (boolean ? 'true' : 'false'), + date: date => `'${date.toISOString()}'`, + undefined: () => 'undefined', + null: () => 'null', + + string(string) { + const content = JSON.stringify(string).slice(1, -1); + return `'${content.replace(/'/g, '\\\'')}'`; + }, + + array(array) { + let result = '['; + + for (let index = 0; index < array.length; index++) { + const value = array[index]; + if (value !== undefined) { + result += serialize(value); + } + + if (index !== array.length - 1) { + result += ','; + } + } + + return result + ']'; + }, + + object(object) { + let result = '{'; + let firstKey = true; + + const objectKeys = Object.keys(object); + const objectKeysCount = objectKeys.length; + + for (let i = 0; i < objectKeysCount; i++) { + let key = objectKeys[i]; + const value = serialize(object[key]); + + if (value === '' || value === 'undefined') { + continue; + } + + if (!/^[a-zA-Z_]\w*$/.test(key)) { + key = serialize.types.string(key); + } + + if (firstKey) { + firstKey = false; + } else { + result += ','; + } + + result += key + ':' + value; + } + + return result + '}'; + } +}; diff --git a/lib/object-serialization.js b/lib/object-serialization.js deleted file mode 100644 index cdb0a64..0000000 --- a/lib/object-serialization.js +++ /dev/null @@ -1,44 +0,0 @@ -'use strict'; - -const vm = require('vm'); -const serializerFactory = require('./serializer-factory'); - -const PARSE_TIMEOUT = 30; - -const serializer = {}; -module.exports = serializer; - -// Parse a string representing an object in JSTP Object Serialization format -// string - a string to parse -// -serializer.interprete = (string) => { - const sandbox = vm.createContext({}); - const script = vm.createScript(`'use strict';(${string})`); - const exported = script.runInNewContext(sandbox, { - timeout: PARSE_TIMEOUT - }); - - const isObject = typeof(exported) === 'object' && exported !== null; - if (isObject && !Array.isArray(exported)) { - const keys = Object.keys(exported); - const keysLength = keys.length; - for (let i = 0; i < keysLength; i++) { - const key = keys[i]; - sandbox[key] = exported[key]; - } - } - - return exported; -}; - -// Serialize a JavaScript object into a string in JSTP Object Serialization -// format -// -serializer.dump = serializerFactory.createSerializer({ - date(date) { - const string = date.toISOString(); - return `new Date('${string}')`; - }, - - function: fn => fn.toString() -}); diff --git a/lib/record-serialization-fallback.js b/lib/record-serialization-fallback.js index 309c224..aa89818 100644 --- a/lib/record-serialization-fallback.js +++ b/lib/record-serialization-fallback.js @@ -1,6 +1,6 @@ 'use strict'; -const serializerFactory = require('./serializer-factory'); +const serialize = require('./json5-serialize'); const jsrs = {}; module.exports = jsrs; @@ -8,7 +8,7 @@ module.exports = jsrs; // Serialize a JavaScript value using the JSTP Record Serialization format // and return a string representing it. // -jsrs.stringify = serializerFactory.createSerializer(); +jsrs.stringify = serialize; // Deserialize a string in the JSTP Record Serialization format into // a JavaScript value and return it. diff --git a/lib/record-serialization.js b/lib/record-serialization.js index 37ebffc..1f11359 100644 --- a/lib/record-serialization.js +++ b/lib/record-serialization.js @@ -1,7 +1,7 @@ 'use strict'; const safeRequire = require('./common').safeRequire; -const serializerFactory = require('./serializer-factory'); +const serialize = require('./json5-serialize'); const jsrs = {}; module.exports = jsrs; @@ -19,7 +19,7 @@ const jstpNative = if (jstpNative) { Object.assign(jsrs, jstpNative); if (!USE_NATIVE_SERIALIZER) { - jsrs.stringify = serializerFactory.createSerializer(); + jsrs.stringify = serialize; } } else { console.warn( diff --git a/lib/serializer-factory.js b/lib/serializer-factory.js deleted file mode 100644 index 611158e..0000000 --- a/lib/serializer-factory.js +++ /dev/null @@ -1,99 +0,0 @@ -'use strict'; - -const factory = {}; -module.exports = factory; - -// Create a serializer function that takes a JavaScript object and returns its -// string representation. By default the behaviour of this function will be -// compliant with the JSTP Record Serialization format but you can override it -// or supply additional types support using the optional argument of this -// factory function. -// additionalTypes - an object with keys which names specify data types and -// values that are functions that serialize values of -// the corresponding types -// -factory.createSerializer = (additionalTypes) => { - function serialize(object) { - let type; - if (Array.isArray(object)) { - type = 'array'; - } else if (object instanceof Date) { - type = 'date'; - } else if (object === null) { - type = 'null'; - } else { - type = typeof(object); - } - - const serializer = serialize.types[type]; - if (serializer) { - return serializer(object); - } - - return ''; - } - - serialize.types = { - number: number => number + '', - boolean: boolean => (boolean ? 'true' : 'false'), - date: date => `'${date.toISOString()}'`, - undefined: () => 'undefined', - null: () => 'null', - - string(string) { - const content = JSON.stringify(string).slice(1, -1); - return `'${content.replace(/'/g, '\\\'')}'`; - }, - - array(array) { - let result = '['; - - for (let index = 0; index < array.length; index++) { - const value = array[index]; - if (value !== undefined) { - result += serialize(value); - } - - if (index !== array.length - 1) { - result += ','; - } - } - - return result + ']'; - }, - - object(object) { - let result = '{'; - let firstKey = true; - - const objectKeys = Object.keys(object); - const objectKeysCount = objectKeys.length; - - for (let i = 0; i < objectKeysCount; i++) { - let key = objectKeys[i]; - const value = serialize(object[key]); - - if (value === '' || value === 'undefined') { - continue; - } - - if (!/^[a-zA-Z_]\w*$/.test(key)) { - key = serialize.types.string(key); - } - - if (firstKey) { - firstKey = false; - } else { - result += ','; - } - - result += key + ':' + value; - } - - return result + '}'; - } - }; - - Object.assign(serialize.types, additionalTypes); - return serialize; -}; diff --git a/test/unit/serializer.test.js b/test/unit/serializer.test.js deleted file mode 100644 index 05259b8..0000000 --- a/test/unit/serializer.test.js +++ /dev/null @@ -1,189 +0,0 @@ -'use strict'; - -const expect = require('chai').expect; - -const jstp = require('../..'); - -function runTestCase(testFunction, testCase) { - testCase.forEach((test) => { - const title = test[0]; - - it(title, () => { - if (Array.isArray(test[1])) { - test[1].forEach((subTest) => { - runTest(subTest[0], subTest[1]); - }); - } else { - runTest(test[1], test[2]); - } - }); - }); - - function runTest(source, target) { - const serialized = jstp[testFunction](source); - expect(serialized).to.eql(target); - } -} - -function swapTestCase(serializationTestCase) { - const deserializationTestCase = []; - - serializationTestCase.forEach((serializationTest) => { - const title = serializationTest[0].replace('serialize', 'deserialize'); - const deserializationTest = [title]; - - if (Array.isArray(serializationTest[1])) { - const tests = []; - deserializationTest.push(tests); - serializationTest[1].forEach((test) => { - tests.push([test[1], test[0]]); - }); - } else { - deserializationTest.push(serializationTest[2]); - deserializationTest.push(serializationTest[1]); - } - - deserializationTestCase.push(deserializationTest); - }); - - return deserializationTestCase; -} - -function skipFunctionTests(testCase) { - return testCase.filter((test) => { - const title = test[0]; - return title.indexOf('function') === -1; - }); -} - -const marcusObject = { - name: 'Marcus Aurelius', - passport: 'AE127095', - birth: { - date: new Date('1990-02-15'), - place: 'Rome' - }, - address: { - country: 'Ukraine', - city: 'Kiev', - zip: '03056', - street: 'Pobedy', - building: '37', - floor: '1', - room: '158' - } -}; - -const marcusObjectString = '{name:\'Marcus Aurelius\',passport:\'AE127095\',' + - 'birth:{date:new Date(\'1990-02-15T00:00:00.000Z\'),place:\'Rome\'},' + - 'address:{country:\'Ukraine\',city:\'Kiev\',zip:\'03056\',' + - 'street:\'Pobedy\',building:\'37\',floor:\'1\',room:\'158\'}}'; - -const recordCommonTestCase = [ - ['must correctly serialize an empty object', {}, '{}'], - ['must serialize numbers', [ - [0, '0'], [42, '42'], [-3, '-3'], [1e100, '1e+100'], [1e-3, '0.001'] - ]], - ['must serialize strings', [ - ['str', '\'str\''], - ['first\nsecond', '\'first\\nsecond\''], - ['it\'s', '\'it\\\'s\''], - ['01\u0000\u0001', '\'01\\u0000\\u0001\''] - ]], - ['must serialize booleans', [ - [true, 'true'], - [false, 'false'] - ]], - ['must serialize arrays', [ - [[], '[]'], - [[1, 2, 3], '[1,2,3]'], - [['outer', ['inner']], '[\'outer\',[\'inner\']]'] - ]], - ['must serialize undefined value', undefined, 'undefined'], - ['must serialize null value', null, 'null'], - ['must not omit null fields of an object', - { field1: 'value', field2: null }, - '{field1:\'value\',field2:null}' ] -]; - -const recordSerializationTestCase = recordCommonTestCase.concat([ - ['must serialize sparse arrays', - // eslint-disable-next-line no-sparse-arrays - [[[1, , 3], '[1,,3]']]], - ['must omit undefined fields of an object', - { field1: 'value', field2: undefined }, - '{field1:\'value\'}' ], - ['must omit functions', - { key: 42, fn() {} }, '{key:42}'], - ['must serialize non-identifier keys', - { '*': 42 }, '{\'*\':42}'] -]); - -const recordDeserializationTestCase = - swapTestCase(recordCommonTestCase).concat([ - ['must skip whitespace', - '{ key:\n\t42 }', { key: 42 }], - ['must parse single-quoted keys', - '{\'key\': 42}', { key: 42 }], - ['must parse double-quoted keys', - '{"key": 42}', { key: 42 }], - ['must parse binary numbers', - '0b1010', 10], - ['must parse octal numbers', - '0o123', 83], - ['must parse hexadecimal numbers', [ - ['0xff', 0xff], - ['0xAF', 0xaf] - ]], - ['must parse Unicode code point escapes', - '\'\\u{1F49A}\\u{1F49B}\'', '💚💛'] - ]); - -const baseObjectSerializationTestCase = - skipFunctionTests(recordSerializationTestCase); - -const additionalObjectSerializationTestCase = [ - ['must serialize dates', - new Date(1473249597286), 'new Date(\'2016-09-07T11:59:57.286Z\')'], - ['must correctly serialize a complex object', - marcusObject, marcusObjectString] -]; - -const baseObjectDeserializationTestCase = - skipFunctionTests(recordDeserializationTestCase); - -const additionalObjectDeserializationTestCase = - swapTestCase(additionalObjectSerializationTestCase); - -function testSyntaxError(parseFunction) { - it('must throw error on illegal input', () => { - [ 'asdf', - 'process', - 'module', - '#+', - '\'\\u{\'', - '\'\\u{}\'' - ].map(input => jstp[parseFunction].bind(null, input)).forEach((fn) => { - expect(fn).to.throw(); - }); - }); -} - -describe('JSTP Serializer and Deserializer', () => { - describe('JSTP Object Serialization', () => { - describe('jstp.dump', () => { - runTestCase('dump', baseObjectSerializationTestCase); - runTestCase('dump', additionalObjectSerializationTestCase); - }); - describe('jstp.interprete', () => { - runTestCase('interprete', baseObjectDeserializationTestCase); - runTestCase('interprete', additionalObjectDeserializationTestCase); - runTestCase('interprete', [ - ['must deserialize sparse arrays to sparse arrays', - // eslint-disable-next-line no-sparse-arrays - '[1,,3]', [1, , 3]], - ]); - testSyntaxError('interprete'); - }); - }); -});