├── .eslintignore ├── .eslintrc ├── .gitignore ├── .npmignore ├── .travis.yml ├── LICENSE ├── README.md ├── index.js ├── package.json └── test ├── .eslintrc ├── handlers.js ├── mocha.opts ├── shared.js └── test.js /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/* 2 | coverage/* 3 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "node": true 4 | }, 5 | "rules": { 6 | "strict": 0, 7 | "quotes": 0, 8 | "indent": [2, 2], 9 | "curly": [2, "multi-line"], 10 | "no-use-before-define": [1, "nofunc"], 11 | "no-unused-vars": [2, "all"], 12 | "no-mixed-requires": [1, true], 13 | "max-depth": [1, 5], 14 | "max-len": [1, 80, 4], 15 | "max-params": [1, 6], 16 | "max-statements": [1, 20], 17 | "eqeqeq": 0, 18 | "new-cap": 0, 19 | "no-else-return": 1, 20 | "no-eq-null": 1, 21 | "no-lonely-if": 1, 22 | "no-path-concat": 0, 23 | "comma-dangle": 0, 24 | "complexity": [1, 20], 25 | "no-floating-decimal": 1, 26 | "no-void": 1, 27 | "no-sync": 1, 28 | "consistent-this": [1, "nope-dont-capture-this"], 29 | "max-nested-callbacks": [2, 3], 30 | "no-nested-ternary": 1, 31 | "space-after-keywords": [1, "always"], 32 | "space-before-function-paren": [1, "never"], 33 | "spaced-line-comment": [1, "always"] 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | .nyc_output 16 | 17 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 18 | .grunt 19 | 20 | # node-waf configuration 21 | .lock-wscript 22 | 23 | # Compiled binary addons (http://nodejs.org/api/addons.html) 24 | build/Release 25 | 26 | # Dependency directory 27 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 28 | node_modules 29 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | coverage 2 | examples 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | sudo: false 3 | node_js: 4 | - "8" 5 | - "10" 6 | - "12" 7 | - "node" 8 | script: 9 | - "npm run lint" 10 | - "npm run coverage" 11 | after_script: "cat ./coverage/lcov.info | ./node_modules/.bin/coveralls" 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Glen Mailer 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # transit-immutable-js 2 | 3 | [Transit](https://github.com/cognitect/transit-js) serialisation for [Immutable.js](https://facebook.github.io/immutable-js/). 4 | 5 | Transit is a serialisation format which builds on top of JSON to provide a richer set of types. It is extensible, which makes it a good choice for easily providing serialisation and deserialisation capabilities for Immutable's types. 6 | 7 | [![npm version](https://img.shields.io/npm/v/transit-immutable-js.svg)](https://www.npmjs.com/package/transit-immutable-js) [![Build Status](https://img.shields.io/travis/glenjamin/transit-immutable-js/master.svg)](https://travis-ci.org/glenjamin/transit-immutable-js) [![Coverage Status](https://coveralls.io/repos/glenjamin/transit-immutable-js/badge.svg?branch=master)](https://coveralls.io/r/glenjamin/transit-immutable-js?branch=master) ![MIT Licensed](https://img.shields.io/npm/l/transit-immutable-js.svg) 8 | 9 | ## Install 10 | 11 | ```sh 12 | npm install transit-immutable-js 13 | ``` 14 | 15 | You must also be using `immutable` for this to be of any use. 16 | 17 | I have chosen to apply very broad npm peerDependencies for simplicity, please check that the versions you have pulled in actually work. 18 | 19 | ## Usage 20 | 21 | ```js 22 | var transit = require('transit-immutable-js'); 23 | var Immutable = require('immutable'); 24 | 25 | var m = Immutable.Map({with: "Some", data: "In"}); 26 | 27 | var str = transit.toJSON(m); 28 | 29 | console.log(str) 30 | // ["~#cmap",["with","Some","data","In"]] 31 | 32 | var m2 = transit.fromJSON(str); 33 | 34 | console.log(Immutable.is(m, m2)); 35 | // true 36 | ``` 37 | 38 | This library also manages to preserve objects which are a mixture of plain javascript and Immutable. 39 | 40 | ```js 41 | var obj = { 42 | iMap: Immutable.Map().set(Immutable.List.of(1, 2, 3), "123"), 43 | iList: Immutable.List.of("a", "b", "c"), 44 | array: [ "javascript", 4, "lyfe" ] 45 | } 46 | 47 | console.log(transit.fromJSON(transit.toJSON(obj))); 48 | // { iMap: Map { [1,2,3]: "123" }, 49 | // iList: List [ "a", "b", "c" ], 50 | // array: [ 'javascript', 4, 'lyfe' ] } 51 | ``` 52 | 53 | ### Usage with transit directly 54 | 55 | As well as the nice friendly wrapped API, the internal handlers are exposed in 56 | case you need to work directly with the `transit-js` API. 57 | 58 | ```js 59 | var transitJS = require('transit-js'); 60 | var handlers = require('transit-immutable-js').handlers; 61 | 62 | var reader = transitJS.reader('json', {handlers: handlers.read}); 63 | var writer = transitJS.writer('json-verbose', {handlers: handlers.write}); 64 | ``` 65 | 66 | ## API 67 | 68 | ### `transit.toJSON(object) => string` 69 | 70 | Convert an immutable object into a JSON representation ([XSS Warning](#xss-warning)) 71 | 72 | ### `transit.fromJSON(string) => object` 73 | 74 | Convert a JSON representation back into an immutable object 75 | 76 | ### `transit.handlers.read` `object` 77 | 78 | A mapping of tags to decoding functions which can be used to create a transit reader directly. 79 | 80 | ### `transit.handlers.write` `transit.map` 81 | 82 | A mapping of type constructors to encoding functions which can be used to create a transit writer directly. 83 | 84 | **The various `withXXX` methods can be combined as desired by chaining them together.** 85 | 86 | ### `transit.withExtraHandlers(Array handlers) => transit` 87 | > Also `transit.handlers.withExtraHandlers(Array handlers) => handlers` 88 | 89 | Create a modified version of the transit API that knows about more types than it did before. This is primarily useful if you have additional custom datatypes that you want to be able serialise and deserialise. Each entry in this array must be an object with the following properties: 90 | 91 | * `tag` *string* - a unique identifier for this type that will be used in the serialised output 92 | * `class` *function* - a constructor function that can be used to identify the type via an `instanceof` check 93 | * `write` *function(value)* - a function which will receive an instance of your type, and is expected to create some serialisable representation of it 94 | * `read` *function(rep)* - a function which will receive the serialisable representation, and is expected to create a new instance from it 95 | 96 | The `read` and `write` functions should form a matched pair of functions - calling read on the result of write should produce the same value and vice versa. Transit applies encoding and decoding recursively, so you can return any type transit understands from `write`, and expect to receive it back in `read` later. 97 | 98 | ### `transit.withFilter(function) => transit` 99 | > Also `transit.handlers.withFilter(function) => handlers` 100 | 101 | Create a modified version of the transit API that deeply applies the provided filter function to all immutable collections before serialising. Can be used to exclude entries. 102 | 103 | ### `transit.withRecords(Array recordClasses, missingRecordHandler = null) => transit` 104 | > Also `transit.handlers.withRecords(Array recordClasses, missingRecordHandler = null) => handlers` 105 | 106 | Creates a modified version of the transit API with support for serializing/deserializing [Record](https://facebook.github.io/immutable-js/docs/#/) objects. If a Record is included in an object to be serialized without the proper handler, on encoding it will be encoded as an `Immutable.Map`. 107 | 108 | `missingRecordHandler` is called when a record-name is not found and can be used to handle the missing record manually. If no handler is given, the deserialisation process will throw an error. It accepts 2 parameters: `name` and `value` and the return value will be used instead of the missing record. 109 | 110 | 111 | ## Example `Record` Usage: 112 | 113 | ```js 114 | var FooRecord = Immutable.Record({ 115 | a: 1, 116 | b: 2, 117 | }, 'foo'); 118 | 119 | var data = new FooRecord(); 120 | 121 | var recordTransit = transit.withRecords([FooRecord]); 122 | var encodedJSON = recordTransit.toJSON(data); 123 | ``` 124 | 125 | ## Example missing `Record` Usage: 126 | 127 | ```js 128 | var BarRecord = Immutable.Record({ 129 | c: '1', 130 | d: '2' 131 | }, 'bar'); 132 | 133 | var FooRecord = Immutable.Record({ 134 | a: 1, 135 | b: 2, 136 | }, 'foo'); 137 | 138 | var data = new FooRecord({a: 3, b: 4}); 139 | 140 | var recordTransitFoo = transit.withRecords([FooRecord]); 141 | var encodedJSON = recordTransitFoo.toJSON(data); 142 | 143 | var recordTransitEmpty = transit.withRecords([], function (name, value) { 144 | switch (name) { 145 | case 'foo': 146 | return new BarRecord({c: value.a, d: value.b}); 147 | default: 148 | return null; 149 | } 150 | }); 151 | 152 | var decodedResult = recordTransitEmpty.fromJSON(encodedJSON); // returns new BarRecord({c: 3, d: 4}) 153 | ``` 154 | 155 | ## XSS Warning 156 | 157 | When embedding JSON in an html page or related context (e.g. css, element attributes, etc), _**care must be taken to sanitize the output**_. By design, niether transit-js nor transit-immutable-js provide output sanitization. 158 | 159 | There are a number of libraries that can help. Including: [xss-filters](https://www.npmjs.com/package/xss-filters), [secure-filters](https://www.npmjs.com/package/secure-filters), and [many more](https://www.npmjs.com/browse/keyword/xss) 160 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var transit = require('transit-js'); 2 | var Immutable = require('immutable'); 3 | 4 | function createReader(handlers) { 5 | return transit.reader('json', { 6 | mapBuilder: { 7 | init: function() { 8 | return {}; 9 | }, 10 | add: function(m, k, v) { 11 | m[k] = v; 12 | return m; 13 | }, 14 | finalize: function(m) { 15 | return m; 16 | } 17 | }, 18 | handlers: handlers 19 | }); 20 | } 21 | 22 | function createReaderHandlers(extras, recordMap, missingRecordHandler) { 23 | var handlers = { 24 | iM: function(v) { 25 | var m = Immutable.Map().asMutable(); 26 | for (var i = 0; i < v.length; i += 2) { 27 | m = m.set(v[i], v[i + 1]); 28 | } 29 | return m.asImmutable(); 30 | }, 31 | iOM: function(v) { 32 | var m = Immutable.OrderedMap().asMutable(); 33 | for (var i = 0; i < v.length; i += 2) { 34 | m = m.set(v[i], v[i + 1]); 35 | } 36 | return m.asImmutable(); 37 | }, 38 | iL: function(v) { 39 | return Immutable.List(v); 40 | }, 41 | iS: function(v) { 42 | return Immutable.Set(v); 43 | }, 44 | iStk: function(v) { 45 | return Immutable.Stack(v); 46 | }, 47 | iOS: function(v) { 48 | return Immutable.OrderedSet(v); 49 | }, 50 | iR: function(v) { 51 | var RecordType = recordMap[v.n]; 52 | if (!RecordType) { 53 | return missingRecordHandler(v.n, v.v); 54 | } 55 | 56 | return new RecordType(v.v); 57 | } 58 | }; 59 | extras.forEach(function(extra) { 60 | handlers[extra.tag] = extra.read; 61 | }); 62 | return handlers; 63 | } 64 | 65 | function createWriter(handlers) { 66 | return transit.writer('json', { 67 | handlers: handlers 68 | }); 69 | } 70 | 71 | function createWriterHandlers(extras, recordMap, predicate) { 72 | function mapSerializer(m) { 73 | var i = 0; 74 | if (predicate) { 75 | m = m.filter(predicate); 76 | } 77 | var a = new Array(2 * m.size); 78 | m.forEach(function(v, k) { 79 | a[i++] = k; 80 | a[i++] = v; 81 | }); 82 | return a; 83 | } 84 | 85 | var handlers = transit.map([ 86 | Immutable.OrderedMap, transit.makeWriteHandler({ 87 | tag: function() { 88 | return 'iOM'; 89 | }, 90 | rep: mapSerializer 91 | }), 92 | Immutable.Map, transit.makeWriteHandler({ 93 | tag: function() { 94 | return 'iM'; 95 | }, 96 | rep: mapSerializer 97 | }), 98 | Immutable.List, transit.makeWriteHandler({ 99 | tag: function() { 100 | return "iL"; 101 | }, 102 | rep: function(v) { 103 | if (predicate) { 104 | v = v.filter(predicate); 105 | } 106 | return v.toArray(); 107 | } 108 | }), 109 | Immutable.Stack, transit.makeWriteHandler({ 110 | tag: function() { 111 | return "iStk"; 112 | }, 113 | rep: function(v) { 114 | if (predicate) { 115 | v = v.filter(predicate); 116 | } 117 | return v.toArray(); 118 | } 119 | }), 120 | Immutable.OrderedSet, transit.makeWriteHandler({ 121 | tag: function() { 122 | return "iOS"; 123 | }, 124 | rep: function(v) { 125 | if (predicate) { 126 | v = v.filter(predicate); 127 | } 128 | return v.toArray(); 129 | } 130 | }), 131 | Immutable.Set, transit.makeWriteHandler({ 132 | tag: function() { 133 | return "iS"; 134 | }, 135 | rep: function(v) { 136 | if (predicate) { 137 | v = v.filter(predicate); 138 | } 139 | return v.toArray(); 140 | } 141 | }), 142 | Function, transit.makeWriteHandler({ 143 | tag: function() { 144 | return '_'; 145 | }, 146 | rep: function() { 147 | return null; 148 | } 149 | }), 150 | "default", transit.makeWriteHandler({ 151 | tag: function() { 152 | return 'iM'; 153 | }, 154 | rep: function(m) { 155 | if ((Immutable.isImmutable && Immutable.isImmutable(m)) || ('toMap' in m)) { 156 | return mapSerializer(Immutable.Map(m)); 157 | } 158 | var e = "Error serializing unrecognized object " + m.toString(); 159 | throw new Error(e); 160 | } 161 | }) 162 | ]); 163 | 164 | Object.keys(recordMap).forEach(function(name) { 165 | handlers.set(recordMap[name], makeRecordHandler(name, predicate)); 166 | }); 167 | 168 | extras.forEach(function(extra) { 169 | handlers.set(extra.class, transit.makeWriteHandler({ 170 | tag: function() { return extra.tag; }, 171 | rep: extra.write 172 | })); 173 | }); 174 | 175 | return handlers; 176 | } 177 | 178 | function validateExtras(extras) { 179 | if (!Array.isArray(extras)) { 180 | invalidExtras(extras, "Expected array of handlers, got %j"); 181 | } 182 | extras.forEach(function(extra) { 183 | if (typeof extra.tag !== "string") { 184 | invalidExtras(extra, 185 | "Expected %j to have property 'tag' which is a string"); 186 | } 187 | if (typeof extra.class !== "function") { 188 | invalidExtras(extra, 189 | "Expected %j to have property 'class' which is a constructor function"); 190 | } 191 | if (typeof extra.write !== "function") { 192 | invalidExtras(extra, 193 | "Expected %j to have property 'write' which is a function"); 194 | } 195 | if (typeof extra.read !== "function") { 196 | invalidExtras(extra, 197 | "Expected %j to have property 'write' which is a function"); 198 | } 199 | }); 200 | } 201 | function invalidExtras(data, msg) { 202 | var json = JSON.stringify(data); 203 | throw new Error(msg.replace("%j", json)); 204 | } 205 | 206 | function recordName(record) { 207 | /* eslint no-underscore-dangle: 0 */ 208 | /* istanbul ignore next */ 209 | return record._name || record.constructor.name || 'Record'; 210 | } 211 | 212 | function makeRecordHandler(name) { 213 | return transit.makeWriteHandler({ 214 | tag: function() { 215 | return 'iR'; 216 | }, 217 | rep: function(m) { 218 | return { 219 | n: name, 220 | v: m.toObject() 221 | }; 222 | } 223 | }); 224 | } 225 | 226 | function buildRecordMap(recordClasses) { 227 | var recordMap = {}; 228 | 229 | recordClasses.forEach(function(RecordType) { 230 | var rec = new RecordType(); 231 | var recName = recordName(rec); 232 | 233 | if (!recName || recName === 'Record') { 234 | throw new Error('Cannot (de)serialize Record() without a name'); 235 | } 236 | 237 | if (recordMap[recName]) { 238 | throw new Error('There\'s already a constructor for a Record named ' + 239 | recName); 240 | } 241 | recordMap[recName] = RecordType; 242 | }); 243 | 244 | return recordMap; 245 | } 246 | 247 | function defaultMissingRecordHandler(recName) { 248 | var msg = 'Tried to deserialize Record type named `' + recName + '`, ' + 249 | 'but no type with that name was passed to withRecords()'; 250 | throw new Error(msg); 251 | } 252 | 253 | function createInstanceFromHandlers(handlers) { 254 | var reader = createReader(handlers.read); 255 | var writer = createWriter(handlers.write); 256 | 257 | return { 258 | toJSON: function toJSON(data) { 259 | return writer.write(data); 260 | }, 261 | fromJSON: function fromJSON(json) { 262 | return reader.read(json); 263 | }, 264 | withExtraHandlers: function(extra) { 265 | return createInstanceFromHandlers(handlers.withExtraHandlers(extra)); 266 | }, 267 | withFilter: function(predicate) { 268 | return createInstanceFromHandlers(handlers.withFilter(predicate)); 269 | }, 270 | withRecords: function(recordClasses, missingRecordHandler) { 271 | return createInstanceFromHandlers( 272 | handlers.withRecords(recordClasses, missingRecordHandler) 273 | ); 274 | } 275 | }; 276 | } 277 | 278 | function createHandlers(options) { 279 | var records = options.records || {}; 280 | var filter = options.filter || false; 281 | var missingRecordFn = options.missingRecordHandler 282 | || defaultMissingRecordHandler; 283 | var extras = options.extras || []; 284 | 285 | return { 286 | read: createReaderHandlers(extras, records, missingRecordFn), 287 | write: createWriterHandlers(extras, records, filter), 288 | withExtraHandlers: function(moreExtras) { 289 | validateExtras(moreExtras); 290 | 291 | return createHandlers({ 292 | extras: extras.concat(moreExtras), 293 | records: records, 294 | filter: filter, 295 | missingRecordHandler: missingRecordFn 296 | }); 297 | }, 298 | withFilter: function(newFilter) { 299 | return createHandlers({ 300 | extras: extras, 301 | records: records, 302 | filter: newFilter, 303 | missingRecordHandler: missingRecordFn 304 | }); 305 | }, 306 | withRecords: function(recordClasses, missingRecordHandler) { 307 | var recordMap = buildRecordMap(recordClasses); 308 | return createHandlers({ 309 | extras: extras, 310 | records: recordMap, 311 | filter: filter, 312 | missingRecordHandler: missingRecordHandler 313 | }); 314 | } 315 | }; 316 | } 317 | 318 | module.exports = createInstanceFromHandlers(createHandlers({})); 319 | module.exports.handlers = createHandlers({}); 320 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "transit-immutable-js", 3 | "version": "0.8.0", 4 | "description": "Transit serialisation for Immutable.js", 5 | "main": "index.js", 6 | "scripts": { 7 | "test:3": "npm install --no-save immutable@^3.7.4 && mocha", 8 | "test:4": "npm install --no-save immutable@^4.0.0-rc.12 && mocha", 9 | "test": "npm run test:3 && npm run test:4", 10 | "lint": "eslint .", 11 | "coverage": "nyc npm test && nyc report --reporter=lcov --reporter=json" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "https://github.com/glenjamin/transit-immutable-js.git" 16 | }, 17 | "author": "Glen Mailer ", 18 | "license": "MIT", 19 | "bugs": { 20 | "url": "https://github.com/glenjamin/transit-immutable-js/issues" 21 | }, 22 | "homepage": "https://github.com/glenjamin/transit-immutable-js", 23 | "peerDependencies": { 24 | "immutable": ">= 3", 25 | "transit-js": ">= 0.8" 26 | }, 27 | "devDependencies": { 28 | "chai": "^2.3.0", 29 | "chai-immutable": "^1.2.0", 30 | "coveralls": "^2.11.2", 31 | "eslint": "^0.24.1", 32 | "immutable": ">=3.7.4", 33 | "mocha": "^2.2.5", 34 | "nyc": "^14.1.1", 35 | "transit-js": "^0.8.807" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /test/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | env: { 3 | "mocha": true 4 | }, 5 | rules: { 6 | "max-nested-callbacks": 0 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /test/handlers.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | var shared = require("./shared"); 3 | 4 | var expect = shared.expect; 5 | var samples = shared.samples; 6 | var expectImmutableEqual = shared.expectImmutableEqual; 7 | 8 | var transitJS = require('transit-js'); 9 | 10 | var handlers = require('../').handlers; 11 | 12 | var reader = transitJS.reader('json', { handlers: handlers.read }); 13 | var writer = transitJS.writer('json', { handlers: handlers.write }); 14 | 15 | describe("direct handlers usage", function() { 16 | 17 | samples.get('Immutable').forEach(function(data, desc) { 18 | describe(desc + " - " + data.inspect(), function() { 19 | it('should encode to JSON', function() { 20 | var json = writer.write(data); 21 | expect(json).to.be.a('string'); 22 | expect(JSON.parse(json)).to.not.eql(null); 23 | }); 24 | it('should round-trip', function() { 25 | var roundTrip = reader.read(writer.write(data)); 26 | expect(roundTrip).to.be.an('object'); 27 | expectImmutableEqual(roundTrip, data); 28 | expect(roundTrip).to.be.an.instanceOf(data.constructor); 29 | }); 30 | }); 31 | }); 32 | 33 | describe("extending handlers", function() { 34 | function Blah(x) { this.x = x; } 35 | var extendedRead = { 36 | blah: function(v) { return new Blah(v); } 37 | }; 38 | Object.keys(handlers.read).forEach(function(tag) { 39 | extendedRead[tag] = handlers.read[tag]; 40 | }); 41 | var extendedWrite = handlers.write.clone(); 42 | extendedWrite.set(Blah, transitJS.makeWriteHandler({ 43 | tag: function() { return 'blah'; }, 44 | rep: function(v) { return v.x; } 45 | })); 46 | 47 | var readerX = transitJS.reader('json', {handlers: extendedRead}); 48 | var writerX = transitJS.writer('json', {handlers: extendedWrite}); 49 | 50 | describe("extended type", function() { 51 | it('should encode to JSON', function() { 52 | var json = writerX.write(new Blah(123)); 53 | expect(json).to.be.a('string'); 54 | expect(JSON.parse(json)).to.not.eql(null); 55 | }); 56 | it('should round-trip', function() { 57 | var roundTrip = readerX.read(writerX.write(new Blah(456))); 58 | expect(roundTrip).to.be.an.instanceOf(Blah); 59 | expect(roundTrip).to.have.property("x", 456); 60 | }); 61 | }); 62 | 63 | samples.get('Immutable').forEach(function(data, desc) { 64 | describe(desc + " - " + data.inspect(), function() { 65 | it('should encode to JSON', function() { 66 | var json = writerX.write(data); 67 | expect(json).to.be.a('string'); 68 | expect(JSON.parse(json)).to.not.eql(null); 69 | }); 70 | it('should round-trip', function() { 71 | var roundTrip = readerX.read(writerX.write(data)); 72 | expect(roundTrip).to.be.an('object'); 73 | expectImmutableEqual(roundTrip, data); 74 | expect(roundTrip).to.be.an.instanceOf(data.constructor); 75 | }); 76 | }); 77 | }); 78 | 79 | }); 80 | }); 81 | -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glenjamin/transit-immutable-js/cb0ac0799d730080ea2403dba4061cf9c9d7b9bd/test/mocha.opts -------------------------------------------------------------------------------- /test/shared.js: -------------------------------------------------------------------------------- 1 | var chai = require('chai'); 2 | var Immutable = require('immutable'); 3 | 4 | chai.use(require('chai-immutable')); 5 | var expect = chai.expect; 6 | exports.expect = expect; 7 | 8 | exports.samples = Immutable.Map({ 9 | 10 | "Immutable": Immutable.Map({ 11 | 12 | "Stack": new Immutable.Stack([1, 2, 3]), 13 | 14 | "Maps": Immutable.Map({"abc": "def\nghi"}), 15 | 16 | "Maps with numeric keys": Immutable.Map().set(1, 2), 17 | 18 | "Maps in Maps": Immutable.Map() 19 | .set(1, Immutable.Map([['X', 'Y'], ['A', 'B']])) 20 | .set(2, Immutable.Map({a: 1, b: 2, c: 3})), 21 | 22 | "Lists": Immutable.List.of(1, 2, 3, 4, 5), 23 | 24 | "Long Lists": Immutable.Range(0, 100).toList(), 25 | 26 | "Lists in Maps": Immutable.Map().set( 27 | Immutable.List.of(1, 2), 28 | Immutable.List.of(1, 2, 3, 4, 5) 29 | ), 30 | 31 | "Sets": Immutable.Set.of(1, 2, 3, 3), 32 | 33 | "OrderedSets": Immutable.OrderedSet.of(1, 4, 3, 3), 34 | 35 | "Ordered Maps": Immutable.OrderedMap() 36 | .set(2, 'a') 37 | .set(3, 'b') 38 | .set(1, 'c') 39 | }), 40 | 41 | JS: Immutable.Map({ 42 | 43 | "array": [1, 2, 3, 4, 5], 44 | 45 | "array of arrays": [ 46 | [1, 2, 3], 47 | [4, 5, 6], 48 | [7, 8, 9, 10] 49 | ], 50 | 51 | "array of immutables": [ 52 | Immutable.Map({1: 2}), 53 | Immutable.List.of(1, 2, 3) 54 | ], 55 | 56 | "object": { 57 | a: 1, 58 | b: 2 59 | }, 60 | 61 | "object of immutables": { 62 | a: Immutable.Map({1: 2}), 63 | b: Immutable.Map({3: 4}) 64 | } 65 | }) 66 | }); 67 | 68 | // This is a hack because records and maps are considered equivalent by 69 | // immutable. 70 | // https://github.com/astorije/chai-immutable/issues/37 71 | function expectImmutableEqual(r1, r2) { 72 | expect(r1).to.eql(r2); 73 | expect(r1.toString()).to.eql(r2.toString()); 74 | } 75 | exports.expectImmutableEqual = expectImmutableEqual; 76 | 77 | function expectNotImmutableEqual(r1, r2) { 78 | try { 79 | expectImmutableEqual(r1, r2); 80 | } catch (ex) { 81 | return true; 82 | } 83 | throw new chai.AssertionError('Expected ' + r1 + ' to differ from ' + r2); 84 | } 85 | exports.expectNotImmutableEqual = expectNotImmutableEqual; 86 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | var shared = require("./shared"); 3 | 4 | var expect = shared.expect; 5 | var samples = shared.samples; 6 | var expectImmutableEqual = shared.expectImmutableEqual; 7 | var expectNotImmutableEqual = shared.expectNotImmutableEqual; 8 | 9 | var Immutable = require('immutable'); 10 | 11 | var transit = require('../'); 12 | 13 | describe('transit', function() { 14 | samples.get('Immutable').forEach(function(data, desc) { 15 | describe(desc + " - " + data.inspect(), function() { 16 | it('should encode to JSON', function() { 17 | var json = transit.toJSON(data); 18 | expect(json).to.be.a('string'); 19 | expect(JSON.parse(json)).to.not.eql(null); 20 | }); 21 | it('should round-trip', function() { 22 | var roundTrip = transit.fromJSON(transit.toJSON(data)); 23 | expect(roundTrip).to.be.an('object'); 24 | expectImmutableEqual(roundTrip, data); 25 | expect(roundTrip).to.be.an.instanceOf(data.constructor); 26 | }); 27 | }); 28 | }); 29 | 30 | samples.get('JS').forEach(function(data, desc) { 31 | describe(desc + " - " + JSON.stringify(data), function() { 32 | it('should encode to JSON', function() { 33 | var json = transit.toJSON(data); 34 | expect(json).to.be.a('string'); 35 | expect(JSON.parse(json)).to.not.eql(null); 36 | }); 37 | it('should round-trip', function() { 38 | var roundTrip = transit.fromJSON(transit.toJSON(data)); 39 | expectImmutableEqual(roundTrip, data); 40 | }); 41 | }); 42 | }); 43 | 44 | it('should ignore functions', function() { 45 | var input = Immutable.Map({ a: function abc(){} }); 46 | var result = transit.fromJSON(transit.toJSON(input)); 47 | expect(result.get('a')).to.eql(null); 48 | }); 49 | 50 | describe('Records', function() { 51 | var FooRecord = Immutable.Record({ 52 | a: 1, 53 | b: 2, 54 | }, 'foo'); 55 | 56 | var BarRecord = Immutable.Record({ 57 | c: '1', 58 | d: '2' 59 | }, 'bar'); 60 | 61 | var NamelessRecord = Immutable.Record({}); 62 | 63 | var ClassyBase = Immutable.Record({name: 'lindsey'}, 'ClassyRecord'); 64 | function ClassyRecord(values) { ClassyBase.call(this, values); } 65 | ClassyRecord.prototype = Object.create(ClassyBase.prototype); 66 | ClassyRecord.prototype.constructor = ClassyRecord; 67 | 68 | var recordTransit = transit.withRecords([FooRecord, BarRecord]); 69 | 70 | it('should ensure maps and records compare differently', function() { 71 | expectNotImmutableEqual(new FooRecord(), Immutable.Map({a: 1, b: 2})); 72 | }); 73 | 74 | it('should round-trip simple records', function() { 75 | var data = Immutable.Map({ 76 | myFoo: new FooRecord(), 77 | myBar: new BarRecord() 78 | }); 79 | 80 | var roundTrip = recordTransit.fromJSON(recordTransit.toJSON(data)); 81 | expectImmutableEqual(data, roundTrip); 82 | 83 | expect(roundTrip.get('myFoo').a).to.eql(1); 84 | expect(roundTrip.get('myFoo').b).to.eql(2); 85 | 86 | expect(roundTrip.get('myBar').c).to.eql('1'); 87 | expect(roundTrip.get('myBar').d).to.eql('2'); 88 | }); 89 | 90 | it('should round-trip complex nested records', function() { 91 | var data = Immutable.Map({ 92 | foo: new FooRecord({ 93 | b: Immutable.List.of(BarRecord(), BarRecord({c: 22})) 94 | }), 95 | bar: new BarRecord() 96 | }); 97 | 98 | var roundTrip = recordTransit.fromJSON(recordTransit.toJSON(data)); 99 | expectImmutableEqual(data, roundTrip); 100 | }); 101 | 102 | it('should serialize unspecified Record as a Map', function() { 103 | var data = Immutable.Map({ 104 | myFoo: new FooRecord(), 105 | myBar: new BarRecord() 106 | }); 107 | 108 | var oneRecordTransit = transit.withRecords([FooRecord]); 109 | var roundTripOneRecord = oneRecordTransit.fromJSON( 110 | oneRecordTransit.toJSON(data)); 111 | 112 | expectImmutableEqual(roundTripOneRecord, Immutable.fromJS({ 113 | myFoo: new FooRecord(), 114 | myBar: {c: '1', d: '2'} 115 | })); 116 | 117 | var roundTripWithoutRecords = transit.fromJSON(transit.toJSON(data)); 118 | 119 | expectImmutableEqual(roundTripWithoutRecords, Immutable.fromJS({ 120 | myFoo: {a: 1, b: 2}, 121 | myBar: {c: '1', d: '2'} 122 | })); 123 | }); 124 | 125 | it('should roundtrip ES6-class-style records', function() { 126 | var data = new ClassyRecord({name: 'jon'}); 127 | 128 | var classyTransit = transit.withRecords([ClassyRecord]); 129 | var roundTrip = classyTransit.fromJSON(classyTransit.toJSON(data)); 130 | 131 | expectImmutableEqual(data, roundTrip); 132 | }); 133 | 134 | it('throws an error when it is passed a record with no name', function() { 135 | expect(function() { 136 | transit.withRecords([NamelessRecord]); 137 | }).to.throw(); 138 | }); 139 | 140 | it('throws an error when it reads an unknown record type', function() { 141 | var input = new FooRecord(); 142 | 143 | var json = recordTransit.toJSON(input); 144 | 145 | var emptyRecordTransit = transit.withRecords([]); 146 | 147 | expect(function() { 148 | emptyRecordTransit.fromJSON(json); 149 | }).to.throw(); 150 | }); 151 | 152 | it('throws an error if two records have the same name', function() { 153 | var R1 = Immutable.Record({}, 'R1'); 154 | var R1_2 = Immutable.Record({}, 'R1'); 155 | 156 | expect(function() { 157 | transit.withRecords([R1, R1_2]); 158 | }).to.throw(); 159 | }); 160 | 161 | it('should not throw an error with custom error-handler', function() { 162 | var input = new FooRecord(); 163 | 164 | var json = recordTransit.toJSON(input); 165 | 166 | var emptyRecordTransit = transit.withRecords([], function() { 167 | return null; 168 | }); 169 | 170 | expect(function() { 171 | emptyRecordTransit.fromJSON(json); 172 | }).to.not.throw(); 173 | }); 174 | 175 | it('should deserializing a FooRecord to BarRecord', function() { 176 | var input = new FooRecord({a: '3', b: '4'}); 177 | 178 | var json = recordTransit.toJSON(input); 179 | 180 | var emptyRecordTransit = transit.withRecords([], function(n, v) { 181 | switch (n) { 182 | case 'foo': 183 | return new BarRecord({c: v.a, d: v.b}); 184 | default: 185 | return null; 186 | } 187 | }); 188 | var result = emptyRecordTransit.fromJSON(json); 189 | 190 | expect(result).to.be.an.instanceof(BarRecord); 191 | expect(result.c).to.eql('3'); 192 | expect(result.d).to.eql('4'); 193 | }); 194 | }); 195 | 196 | describe('.withFilter(predicate)', function(){ 197 | var filterFunction = function(val, key) { 198 | return key[0] !== '_'; 199 | }; 200 | var filter = transit.withFilter(filterFunction); 201 | 202 | it('can ignore Map entries', function() { 203 | var input = Immutable.Map({ 204 | a: 'foo', _b: 'bar', c: Immutable.Map({d: 'deep', _e: 'hide'}) 205 | }); 206 | var result = filter.fromJSON(filter.toJSON(input)); 207 | expect(result.get('a')).to.eql('foo'); 208 | expect(result.get('_b')).to.eql(undefined); 209 | expect(result.size).to.eql(2); 210 | expect(result.getIn(['c', 'd'])).to.eql('deep'); 211 | expect(result.getIn(['c', '_e'])).to.eql(undefined); 212 | expect(result.getIn(['c']).size).to.eql(1); 213 | }); 214 | 215 | it('can ignore OrderedMap entries', function() { 216 | var input = Immutable.OrderedMap() 217 | .set('a', 'baz').set('_b', 'bar') 218 | .set('c', Immutable.OrderedMap({d: 'deep', _e: 'hide'})); 219 | var result = filter.fromJSON(filter.toJSON(input)); 220 | expect(result.get('a')).to.eql('baz'); 221 | expect(result.get('_b')).to.eql(undefined); 222 | expect(result.size).to.eql(2); 223 | expect(result.getIn(['c', 'd'])).to.eql('deep'); 224 | expect(result.getIn(['c', '_e'])).to.eql(undefined); 225 | expect(result.getIn(['c']).size).to.eql(1); 226 | }); 227 | 228 | it('can ignore Set entries', function() { 229 | var input = Immutable.OrderedSet.of(1, 2, 3, 3, 'a'); 230 | filter = transit.withFilter(function(val) { 231 | return typeof val === 'number'; 232 | }); 233 | var result = filter.fromJSON(filter.toJSON(input)); 234 | expect(result.includes('a')).to.eql(false); 235 | expect(result.size).to.eql(3); 236 | }); 237 | 238 | it('can ignore Stack entries', function() { 239 | var input = Immutable.Stack.of(1, 2, 3, 'a'); 240 | filter = transit.withFilter(function(val) { 241 | return typeof val === 'number'; 242 | }); 243 | var result = filter.fromJSON(filter.toJSON(input)); 244 | expect(result.includes('a')).to.eql(false); 245 | expect(result.size).to.eql(3); 246 | }); 247 | 248 | it('can ignore OrderedSet entries', function() { 249 | var input = Immutable.Set.of(1, 2, 3, 3, 'a'); 250 | filter = transit.withFilter(function(val) { 251 | return typeof val === 'number'; 252 | }); 253 | var result = filter.fromJSON(filter.toJSON(input)); 254 | expect(result.includes('a')).to.eql(false); 255 | expect(result.size).to.eql(3); 256 | }); 257 | 258 | it('can ignore List entries', function() { 259 | var input = Immutable.List.of(1, 2, 3, 3, 'a'); 260 | var result = filter.fromJSON(filter.toJSON(input)); 261 | expect(result.includes('a')).to.eql(false); 262 | expect(result.size).to.eql(4); 263 | }); 264 | 265 | it('can ignore Maps nested in Records', function() { 266 | var MyRecord = Immutable.Record({ 267 | a: null, 268 | _b: 'bar' 269 | }, 'myRecord'); 270 | 271 | var input = new MyRecord({a: Immutable.Map({_c: 1, d: 2}), _b: 'baz' }); 272 | var recordFilter = transit 273 | .withRecords([MyRecord]) 274 | .withFilter(filterFunction); 275 | 276 | var result = recordFilter.fromJSON(recordFilter.toJSON(input)); 277 | 278 | expect(result.getIn(['a', 'd'])).to.eql(2); 279 | expect(result.getIn(['a', '_c'])).to.eql(undefined); 280 | expect(result.get('a').size).to.eql(1); 281 | expect(result.get('_b')).to.eql('baz'); 282 | }); 283 | 284 | it('should use missing-record-handler combined with filter', function() { 285 | var FooRecord = Immutable.Record({ 286 | a: 1, 287 | b: 2, 288 | }, 'foo'); 289 | 290 | var BarRecord = Immutable.Record({ 291 | c: '1', 292 | d: '2' 293 | }, 'bar'); 294 | 295 | var input = new Immutable.Map({ 296 | _bar: new BarRecord(), 297 | foo: new FooRecord({ 298 | a: 3, 299 | b: 4 300 | }) 301 | }); 302 | 303 | var missingRecordHandler = function(n, v) { 304 | switch (n) { 305 | case 'foo': 306 | return new BarRecord({c: v.a, d: v.b}); 307 | default: 308 | return null; 309 | } 310 | }; 311 | 312 | var recordFilter = transit 313 | .withRecords([FooRecord, BarRecord]) 314 | .withFilter(filterFunction); 315 | var json = recordFilter.toJSON(input); 316 | recordFilter = transit 317 | .withRecords([BarRecord], missingRecordHandler) 318 | .withFilter(filterFunction); 319 | 320 | var result = recordFilter.fromJSON(json); 321 | 322 | expect(result.get('foo').c).to.eql(3); 323 | expect(result.get('foo').d).to.eql(4); 324 | expect(result.get('_bar')).to.eql(undefined); 325 | }); 326 | 327 | }); 328 | 329 | describe("withExtraHandlers", function() { 330 | function Point2d(x, y) { this.x = x; this.y = y; } 331 | function Point3d(x, y, z) { this.x = x; this.y = y; this.z = z; } 332 | var transitX; 333 | before(function() { 334 | transitX = transit.withExtraHandlers([ 335 | { 336 | tag: "2d", class: Point2d, 337 | write: function(val) { return [val.x, val.y]; }, 338 | read: function(rep) { return new Point2d(rep[0], rep[1]); } 339 | }, 340 | { 341 | tag: "3d", class: Point3d, 342 | write: function(val) { return {x: val.x, y: val.y, z: val.z}; }, 343 | read: function(rep) { return new Point3d(rep.x, rep.y, rep.z); } 344 | } 345 | ]); 346 | }); 347 | var value = Immutable.Map.of( 348 | 123, new Point2d(3, 5), 349 | "co-ords", Immutable.List.of(new Point3d(1, 2, 3), new Point3d(4, 5, 6)) 350 | ); 351 | 352 | it('should encode into json', function() { 353 | var json = transitX.toJSON(value); 354 | expect(json).to.be.a('string'); 355 | expect(JSON.parse(json)).to.not.eql(null); 356 | }); 357 | it('should round-trip', function() { 358 | var roundTrip = transitX.fromJSON(transitX.toJSON(value)); 359 | expect(roundTrip).to.be.an.instanceof(Immutable.Map); 360 | 361 | var point2 = roundTrip.get(123); 362 | expect(point2).to.be.an.instanceof(Point2d); 363 | expect(point2).to.have.property("x", 3); 364 | expect(point2).to.have.property("y", 5); 365 | 366 | var point3a = roundTrip.getIn(["co-ords", 0]); 367 | expect(point3a).to.be.an.instanceof(Point3d); 368 | expect(point3a).to.have.property("x", 1); 369 | expect(point3a).to.have.property("y", 2); 370 | expect(point3a).to.have.property("z", 3); 371 | 372 | var point3b = roundTrip.getIn(["co-ords", 1]); 373 | expect(point3b).to.be.an.instanceof(Point3d); 374 | expect(point3b).to.have.property("x", 4); 375 | expect(point3b).to.have.property("y", 5); 376 | expect(point3b).to.have.property("z", 6); 377 | }); 378 | 379 | describe("argument checking", function() { 380 | function makeExtra(override) { 381 | function ABC() {} 382 | var extra = { 383 | tag: "abc", class: ABC, 384 | read: function(rep) { return ABC(rep); }, 385 | write: function(v) { return v * 0; } 386 | }; 387 | Object.keys(override || {}).forEach(function(key) { 388 | extra[key] = override[key]; 389 | }); 390 | return extra; 391 | } 392 | it("should check for non-array", function() { 393 | expect(function() { 394 | transit.withExtraHandlers({}); 395 | }).to.throw(); 396 | }); 397 | it("should check for string tag", function() { 398 | expect(function() { 399 | transit.withExtraHandlers([ 400 | makeExtra(), 401 | makeExtra({tag: 123}) 402 | ]); 403 | }).to.throw(); 404 | }); 405 | it("should check for class function", function() { 406 | expect(function() { 407 | transit.withExtraHandlers([ 408 | makeExtra({class: "blah"}) 409 | ]); 410 | }).to.throw(); 411 | }); 412 | it("should check for write function", function() { 413 | expect(function() { 414 | transit.withExtraHandlers([ 415 | makeExtra({write: [1, 2, 3]}) 416 | ]); 417 | }).to.throw(); 418 | }); 419 | it("should check for read function", function() { 420 | expect(function() { 421 | transit.withExtraHandlers([ 422 | makeExtra({read: {nope: "not this"}}) 423 | ]); 424 | }).to.throw(); 425 | }); 426 | }); 427 | }); 428 | 429 | describe('Unknown Input', function() { 430 | it('fails when an unrecognized object is passed', function() { 431 | var MyObject = function() {}; 432 | var MyObjectInstance = new MyObject(); 433 | 434 | expect(function() { 435 | transit.toJSON(MyObjectInstance); 436 | }).to.throw(); 437 | }); 438 | }); 439 | 440 | }); 441 | --------------------------------------------------------------------------------