diff --git a/packages/remotedev-serialize/README.md b/packages/remotedev-serialize/README.md new file mode 100644 index 00000000..77f3ccd1 --- /dev/null +++ b/packages/remotedev-serialize/README.md @@ -0,0 +1,115 @@ +# Serialize ImmutableJS data + +### Installation + +``` +yarn add remotedev-serialize +``` + +### Usage with ImmutableJS data structures + +Just pass the Immutable library to our class: + +```js +import Immutable from 'immutable'; +import Serialize from 'remotedev-serialize'; +const { stringify, parse } = Serialize.immutable(Immutable); + +const data = Immutable.fromJS({ foo: 'bar', baz: { qux: 42 } }); +const serialized = stringify(data); +console.log(serialized); +// {"data":{"foo":"bar","baz":{"data":{"qux":42},"__serializedType__":"ImmutableMap"}},"__serializedType__":"ImmutableMap"} +const parsed = parse(serialized); +console.log(Immutable.is(parsed, data)); +// true +``` + +See [the tests](https://github.com/reduxjs/redux-devtools/blob/master/packages/remotedev-serialize/test/immutable.spec.js) for more examples of usage. + +### Usage with ImmutableJS Record classes + +To parse a Record class back, you need to specify a reference to it: + +```js +import Immutable from 'immutable'; +import Serialize from 'remotedev-serialize'; + +const ABRecord = Immutable.Record({ a: 1, b: 2 }); +const { stringify, parse } = Serialize.immutable(Immutable, [ABRecord]); + +const myRecord = new ABRecord({ b: 3 }); +const serialized = stringify(myRecord); +console.log(serialized); +// {"data":{"a":1,"b":3},"__serializedType__":"ImmutableRecord","__serializedRef__":0} +const parsed = parse(serialized); +console.log(Immutable.is(parsed, myRecord)); +// true +``` + +### Passing custom serialization functions + +You can pass custom replacer and reviver functions to Serialize: + +```js +import Immutable from 'immutable'; +import Serialize from 'remotedev-serialize'; + +function customReplacer(key, value, defaultReplacer) { + if (value === 1) { + return { data: 'one', __serializedType__: 'number' }; + } + return defaultReplacer(key, value); +} + +function customReviver(key, value, defaultReviver) { + if ( + typeof value === 'object' && + value.__serializedType__ === 'number' && + value.data === 'one' + ) { + return 1; + } + return defaultReviver(key, value); +} + +const { stringify, parse } = Serialize.immutable( + Immutable, + null, + customReplacer, + customReviver +); + +const map = Immutable.Map({ a: 1, b: 2 }); +const serialized = stringify(map); +console.log(serialized); +// {"data":{"a":{"data":"one","__serializedType__":"number"},"b":2},"__serializedType__":"ImmutableMap"} +const parsed = parse(serialized); +console.log(Immutable.is(parsed, map)); +// true +``` + +### Supported + +#### ImutableJS + +- [x] Record +- [x] Range +- [x] Repeat +- [x] Map +- [x] OrderedMap +- [x] List +- [x] Set +- [x] OrderedSet +- [x] Seq +- [x] Stack + +#### ES6 + +- [x] Symbol +- [x] Map +- [x] Set +- [ ] Typed Array + +### License + +MIT diff --git a/packages/remotedev-serialize/constants/options.js b/packages/remotedev-serialize/constants/options.js new file mode 100644 index 00000000..b02ec8ef --- /dev/null +++ b/packages/remotedev-serialize/constants/options.js @@ -0,0 +1,15 @@ +// jsan stringify options + +module.exports = { + refs: false, // references can't be resolved on the original Immutable structure + date: true, + function: true, + regex: true, + undefined: true, + error: true, + symbol: true, + map: true, + set: true, + nan: true, + infinity: true +}; diff --git a/packages/remotedev-serialize/helpers/index.js b/packages/remotedev-serialize/helpers/index.js new file mode 100644 index 00000000..cef15651 --- /dev/null +++ b/packages/remotedev-serialize/helpers/index.js @@ -0,0 +1,32 @@ +function mark(data, type, transformMethod) { + return { + data: transformMethod ? data[transformMethod]() : data, + __serializedType__: type + }; +} + +function extract(data, type) { + return { + data: Object.assign({}, data), + __serializedType__: type + }; +} + +function refer(data, type, isArray, refs) { + var r = mark(data, type, isArray); + if (!refs) return r; + for (var i = 0; i < refs.length; i++) { + var ref = refs[i]; + if (typeof ref === 'function' && data instanceof ref) { + r.__serializedRef__ = i; + return r; + } + } + return r; +} + +module.exports = { + mark: mark, + extract: extract, + refer: refer +}; diff --git a/packages/remotedev-serialize/immutable/index.js b/packages/remotedev-serialize/immutable/index.js new file mode 100644 index 00000000..dda8966a --- /dev/null +++ b/packages/remotedev-serialize/immutable/index.js @@ -0,0 +1,23 @@ +var jsan = require('jsan'); +var serialize = require('./serialize'); +var options = require('../constants/options'); + +module.exports = function(Immutable, refs, customReplacer, customReviver) { + return { + stringify: function(data) { + return jsan.stringify( + data, + serialize(Immutable, refs, customReplacer, customReviver).replacer, + null, + options + ); + }, + parse: function(data) { + return jsan.parse( + data, + serialize(Immutable, refs, customReplacer, customReviver).reviver + ); + }, + serialize: serialize + }; +}; diff --git a/packages/remotedev-serialize/immutable/serialize.js b/packages/remotedev-serialize/immutable/serialize.js new file mode 100644 index 00000000..600d046f --- /dev/null +++ b/packages/remotedev-serialize/immutable/serialize.js @@ -0,0 +1,87 @@ +var helpers = require('../helpers'); +var mark = helpers.mark; +var extract = helpers.extract; +var refer = helpers.refer; +var options = require('../constants/options'); + +module.exports = function serialize( + Immutable, + refs, + customReplacer, + customReviver +) { + function replacer(key, value) { + if (value instanceof Immutable.Record) + return refer(value, 'ImmutableRecord', 'toObject', refs); + if (value instanceof Immutable.Range) + return extract(value, 'ImmutableRange'); + if (value instanceof Immutable.Repeat) + return extract(value, 'ImmutableRepeat'); + if (Immutable.OrderedMap.isOrderedMap(value)) + return mark(value, 'ImmutableOrderedMap', 'toObject'); + if (Immutable.Map.isMap(value)) + return mark(value, 'ImmutableMap', 'toObject'); + if (Immutable.List.isList(value)) + return mark(value, 'ImmutableList', 'toArray'); + if (Immutable.OrderedSet.isOrderedSet(value)) + return mark(value, 'ImmutableOrderedSet', 'toArray'); + if (Immutable.Set.isSet(value)) + return mark(value, 'ImmutableSet', 'toArray'); + if (Immutable.Seq.isSeq(value)) + return mark(value, 'ImmutableSeq', 'toArray'); + if (Immutable.Stack.isStack(value)) + return mark(value, 'ImmutableStack', 'toArray'); + return value; + } + + function reviver(key, value) { + if ( + typeof value === 'object' && + value !== null && + '__serializedType__' in value + ) { + var data = value.data; + switch (value.__serializedType__) { + case 'ImmutableMap': + return Immutable.Map(data); + case 'ImmutableOrderedMap': + return Immutable.OrderedMap(data); + case 'ImmutableList': + return Immutable.List(data); + case 'ImmutableRange': + return Immutable.Range(data._start, data._end, data._step); + case 'ImmutableRepeat': + return Immutable.Repeat(data._value, data.size); + case 'ImmutableSet': + return Immutable.Set(data); + case 'ImmutableOrderedSet': + return Immutable.OrderedSet(data); + case 'ImmutableSeq': + return Immutable.Seq(data); + case 'ImmutableStack': + return Immutable.Stack(data); + case 'ImmutableRecord': + return refs && refs[value.__serializedRef__] + ? new refs[value.__serializedRef__](data) + : Immutable.Map(data); + default: + return data; + } + } + return value; + } + + return { + replacer: customReplacer + ? function(key, value) { + return customReplacer(key, value, replacer); + } + : replacer, + reviver: customReviver + ? function(key, value) { + return customReviver(key, value, reviver); + } + : reviver, + options: options + }; +}; diff --git a/packages/remotedev-serialize/index.js b/packages/remotedev-serialize/index.js new file mode 100644 index 00000000..01180b95 --- /dev/null +++ b/packages/remotedev-serialize/index.js @@ -0,0 +1,5 @@ +var immutable = require('./immutable'); + +module.exports = { + immutable: immutable +}; diff --git a/packages/remotedev-serialize/package.json b/packages/remotedev-serialize/package.json new file mode 100644 index 00000000..d0fd72ee --- /dev/null +++ b/packages/remotedev-serialize/package.json @@ -0,0 +1,31 @@ +{ + "name": "remotedev-serialize", + "version": "0.1.8", + "description": "Serialize unserializable data and parse it back.", + "main": "index.js", + "scripts": { + "lint": "eslint src test", + "test": "jest --no-cache", + "prepublish": "npm run lint && npm run test" + }, + "repository": { + "type": "git", + "url": "https://github.com/reduxjs/redux-devtools.git" + }, + "keywords": [ + "redux", + "devtools" + ], + "author": "Mihail Diordiev (https://github.com/zalmoxisus)", + "license": "MIT", + "bugs": { + "url": "https://github.com/reduxjs/redux-devtools/issues" + }, + "homepage": "https://github.com/reduxjs/redux-devtools", + "devDependencies": { + "immutable": "^3.8.2" + }, + "dependencies": { + "jsan": "^3.1.13" + } +} diff --git a/packages/remotedev-serialize/test/__snapshots__/helpers.spec.js.snap b/packages/remotedev-serialize/test/__snapshots__/helpers.spec.js.snap new file mode 100644 index 00000000..151e8a9d --- /dev/null +++ b/packages/remotedev-serialize/test/__snapshots__/helpers.spec.js.snap @@ -0,0 +1,33 @@ +exports[`Helpers extract 1`] = ` +Object { + "__serializedType__": "testType", + "data": Object { + "testData": "test", + }, +} +`; + +exports[`Helpers mark 1`] = ` +Object { + "__serializedType__": "testType", + "data": Object { + "testData": "test", + }, +} +`; + +exports[`Helpers mark 2`] = ` +Object { + "__serializedType__": "testType", + "data": "[object Object]", +} +`; + +exports[`Helpers refer 1`] = ` +Object { + "__serializedType__": "testType", + "data": Object { + "testData": "test", + }, +} +`; diff --git a/packages/remotedev-serialize/test/__snapshots__/immutable.spec.js.snap b/packages/remotedev-serialize/test/__snapshots__/immutable.spec.js.snap new file mode 100644 index 00000000..ffae59fd --- /dev/null +++ b/packages/remotedev-serialize/test/__snapshots__/immutable.spec.js.snap @@ -0,0 +1,21 @@ +exports[`Immutable Nested stringify 1`] = `"{\"data\":[[\"map\",{\"data\":{\"seq\":{\"data\":[1,2,3,4,5,6,7,8],\"__serializedType__\":\"ImmutableSeq\"},\"stack\":{\"data\":[\"a\",\"b\",\"c\"],\"__serializedType__\":\"ImmutableStack\"}},\"__serializedType__\":\"ImmutableOrderedMap\"}],[\"repeat\",{\"data\":{\"_value\":\"hi\",\"size\":100},\"__serializedType__\":\"ImmutableRepeat\"}]],\"__serializedType__\":\"ImmutableSet\"}"`; + +exports[`Immutable Record stringify 1`] = `"{\"data\":{\"a\":1,\"b\":3},\"__serializedType__\":\"ImmutableRecord\",\"__serializedRef__\":0}"`; + +exports[`Immutable Stringify list 1`] = `"{\"data\":[1,2,3,4,5,6,7,8,9,10],\"__serializedType__\":\"ImmutableList\"}"`; + +exports[`Immutable Stringify map 1`] = `"{\"data\":{\"a\":1,\"b\":2,\"c\":3,\"d\":4},\"__serializedType__\":\"ImmutableMap\"}"`; + +exports[`Immutable Stringify orderedMap 1`] = `"{\"data\":{\"b\":2,\"a\":1,\"c\":3,\"d\":4},\"__serializedType__\":\"ImmutableOrderedMap\"}"`; + +exports[`Immutable Stringify orderedSet 1`] = `"{\"data\":[10,9,8,7,6,5,4,3,2,1],\"__serializedType__\":\"ImmutableOrderedSet\"}"`; + +exports[`Immutable Stringify range 1`] = `"{\"data\":{\"_start\":0,\"_end\":7,\"_step\":1,\"size\":7},\"__serializedType__\":\"ImmutableRange\"}"`; + +exports[`Immutable Stringify repeat 1`] = `"{\"data\":{\"_value\":\"hi\",\"size\":100},\"__serializedType__\":\"ImmutableRepeat\"}"`; + +exports[`Immutable Stringify seq 1`] = `"{\"data\":[1,2,3,4,5,6,7,8],\"__serializedType__\":\"ImmutableSeq\"}"`; + +exports[`Immutable Stringify set 1`] = `"{\"data\":[1,2,3,4,5,6,7,8,9,10],\"__serializedType__\":\"ImmutableSet\"}"`; + +exports[`Immutable Stringify stack 1`] = `"{\"data\":[\"a\",\"b\",\"c\"],\"__serializedType__\":\"ImmutableStack\"}"`; diff --git a/packages/remotedev-serialize/test/helpers.spec.js b/packages/remotedev-serialize/test/helpers.spec.js new file mode 100644 index 00000000..45d86046 --- /dev/null +++ b/packages/remotedev-serialize/test/helpers.spec.js @@ -0,0 +1,27 @@ +var helpers = require('../helpers'); +var mark = helpers.mark; +var extract = helpers.extract; +var refer = helpers.refer; + +describe('Helpers', function() { + it('mark', function() { + expect(mark({ testData: 'test' }, 'testType')).toMatchSnapshot(); + expect( + mark({ testData: 'test' }, 'testType', 'toString') + ).toMatchSnapshot(); + }); + + it('extract', function() { + expect(extract({ testData: 'test' }, 'testType')).toMatchSnapshot(); + }); + + it('refer', function() { + var TestClass = function(data) { + return data; + }; + var testInstance = new TestClass({ testData: 'test' }); + expect( + refer(testInstance, 'testType', false, [TestClass]) + ).toMatchSnapshot(); + }); +}); diff --git a/packages/remotedev-serialize/test/immutable.spec.js b/packages/remotedev-serialize/test/immutable.spec.js new file mode 100644 index 00000000..c4e4d67b --- /dev/null +++ b/packages/remotedev-serialize/test/immutable.spec.js @@ -0,0 +1,148 @@ +var Immutable = require('immutable'); +var Serialize = require('../immutable'); +var serialize = Serialize(Immutable); +var stringify = serialize.stringify; +var parse = serialize.parse; + +var data = { + map: Immutable.Map({ a: 1, b: 2, c: 3, d: 4 }), + orderedMap: Immutable.OrderedMap({ b: 2, a: 1, c: 3, d: 4 }), + list: Immutable.List([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]), + range: Immutable.Range(0, 7), + repeat: Immutable.Repeat('hi', 100), + set: Immutable.Set([10, 9, 8, 7, 6, 5, 4, 3, 2, 1]), + orderedSet: Immutable.OrderedSet([10, 9, 8, 7, 6, 5, 4, 3, 2, 1]), + seq: Immutable.Seq.of(1, 2, 3, 4, 5, 6, 7, 8), + stack: Immutable.Stack.of('a', 'b', 'c') +}; + +describe('Immutable', function() { + var stringified = {}; + describe('Stringify', function() { + Object.keys(data).forEach(function(key) { + it(key, function() { + stringified[key] = stringify(data[key]); + expect(stringified[key]).toMatchSnapshot(); + }); + }); + }); + + describe('Parse', function() { + Object.keys(data).forEach(function(key) { + it(key, function() { + expect(parse(stringified[key])).toEqual(data[key]); + }); + }); + }); + + describe('Record', function() { + var ABRecord = Immutable.Record({ a: 1, b: 2 }); + var myRecord = new ABRecord({ b: 3 }); + + var serialize = Serialize(Immutable, [ABRecord]); + var stringify = serialize.stringify; + var parse = serialize.parse; + var stringifiedRecord; + + it('stringify', function() { + stringifiedRecord = stringify(myRecord); + expect(stringifiedRecord).toMatchSnapshot(); + }); + + it('parse', function() { + expect(parse(stringifiedRecord)).toEqual(myRecord); + }); + }); + + describe('Nested', function() { + var ABRecord = Immutable.Record({ + map: Immutable.OrderedMap({ seq: data.seq, stack: data.stack }), + repeat: data.repeat + }); + var nestedData = Immutable.Set(ABRecord(), data.orderedSet, data.range); + + var serialize = Serialize(Immutable, [ABRecord]); + var stringify = serialize.stringify; + var parse = serialize.parse; + var stringifiedNested; + + it('stringify', function() { + stringifiedNested = stringify(nestedData); + expect(stringifiedNested).toMatchSnapshot(); + }); + + it('parse', function() { + expect(parse(stringifiedNested)).toEqual(nestedData); + }); + }); + describe('With references', function() { + it('serializes and deserializes', function() { + var sharedValue = []; + var record = Immutable.Record({ + prop: sharedValue + }); + + var refs = [record]; + + var obj = Immutable.Map({ + fst: new record(), + scnd: new record() + }); + + var serialized = stringify( + obj, + Serialize(Immutable, refs).replacer, + null, + true + ); + var parsed = JSON.parse(serialized); + + var fstProp = parsed.data.fst.data.prop; + var scndProp = parsed.data.scnd.data.prop; + + expect(fstProp).toEqual(scndProp); + expect(Array.isArray(obj.get('fst').get('prop'))); + }); + }); + + describe('Custom replacer and reviver functions', function() { + var customOneRepresentation = 'one'; + + function customReplacer(key, value, defaultReplacer) { + if (value === 1) { + return { data: customOneRepresentation, __serializedType__: 'number' }; + } + return defaultReplacer(key, value); + } + + function customReviver(key, value, defaultReviver) { + if ( + typeof value === 'object' && + value.__serializedType__ === 'number' && + value.data === customOneRepresentation + ) { + return 1; + } + return defaultReviver(key, value); + } + + var serializeCustom = Serialize( + Immutable, + null, + customReplacer, + customReviver + ); + + Object.keys(data).forEach(function(key) { + var stringified = serializeCustom.stringify(data[key]); + it(key, function() { + var deserialized = serializeCustom.parse(stringified); + expect(deserialized).toEqual(data[key]); + if (key === 'map' || key === 'orderedMap') { + const deserializedDefault = parse(stringified); + expect(deserializedDefault.get('a')).toEqual(customOneRepresentation); + } + }); + }); + }); +}); diff --git a/yarn.lock b/yarn.lock index 5ea93c7c..58b60c05 100644 --- a/yarn.lock +++ b/yarn.lock @@ -13926,13 +13926,6 @@ relateurl@0.2.x, relateurl@^0.2.7: resolved "https://registry.yarnpkg.com/relateurl/-/relateurl-0.2.7.tgz#54dbf377e51440aca90a4cd274600d3ff2d888a9" integrity sha1-VNvzd+UUQKypCkzSdGANP/LYiKk= -remotedev-serialize@^0.1.8: - version "0.1.8" - resolved "https://registry.yarnpkg.com/remotedev-serialize/-/remotedev-serialize-0.1.8.tgz#c99cb184e7f71a906162abc404be8ce33810205f" - integrity sha512-3YG/FDcOmiK22bl5oMRM8RRnbGrFEuPGjbcDG+z2xi5aQaNQNZ8lqoRnZTwXVfaZtutXuiAQOgPRrogzQk8edg== - dependencies: - jsan "^3.1.13" - remove-trailing-separator@^1.0.1: version "1.1.0" resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef"