diff --git a/packages/remotedev-serialize/README.md b/packages/remotedev-serialize/README.md new file mode 100644 index 00000000..ba44327e --- /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/remote-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..a7479ff5 --- /dev/null +++ b/packages/remotedev-serialize/package.json @@ -0,0 +1,30 @@ +{ + "name": "remotedev-serialize", + "version": "0.1.8", + "description": "Serialize unserializable data and parse it back.", + "main": "index.js", + "scripts": { + "test": "jest --no-cache", + "prepublish": "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..00056f49 --- /dev/null +++ b/packages/remotedev-serialize/test/__snapshots__/helpers.spec.js.snap @@ -0,0 +1,35 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +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..115e2115 --- /dev/null +++ b/packages/remotedev-serialize/test/__snapshots__/immutable.spec.js.snap @@ -0,0 +1,23 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +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..0eb77b07 --- /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') { + var deserializedDefault = parse(stringified); + expect(deserializedDefault.get('a')).toEqual(customOneRepresentation); + } + }); + }); + }); +}); diff --git a/yarn.lock b/yarn.lock index 5ea93c7c..352c788b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4193,7 +4193,7 @@ babel-preset-react-app@^6.1.0: babel-plugin-macros "2.4.2" babel-plugin-transform-react-remove-prop-types "0.4.18" -babel-runtime@6.x.x, babel-runtime@^6.11.6, babel-runtime@^6.2.0, babel-runtime@^6.22.0, babel-runtime@^6.23.0, babel-runtime@^6.26.0, babel-runtime@^6.5.0: +babel-runtime@6.x.x, babel-runtime@^6.11.6, babel-runtime@^6.22.0, babel-runtime@^6.23.0, babel-runtime@^6.26.0, babel-runtime@^6.5.0: version "6.26.0" resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.26.0.tgz#965c7058668e82b55d7bfe04ff2337bc8b5647fe" integrity sha1-llxwWGaOgrVde/4E/yM3vItWR/4= @@ -13783,17 +13783,6 @@ redent@^3.0.0: indent-string "^4.0.0" strip-indent "^3.0.0" -redux-devtools-dock-monitor@^1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/redux-devtools-dock-monitor/-/redux-devtools-dock-monitor-1.1.3.tgz#1205e823c82536570aac8551a1c4b70972cba6aa" - integrity sha512-yAXzoI0lpjv19CxVuw8RECeFWUVdyzayqnkX8ePZyeXV2ZgIk4T+rKx82Wk+REP1y3rl8o1/oFDq4B7EobOqMg== - dependencies: - babel-runtime "^6.2.0" - parse-key "^0.2.1" - prop-types "^15.5.8" - react-dock "^0.2.4" - react-pure-render "^1.0.2" - redux-devtools-themes@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/redux-devtools-themes/-/redux-devtools-themes-1.0.0.tgz#c482dce3c5373976045f40134907d9dcb3ae3d5d" @@ -13926,13 +13915,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"