transit-immutable-js
Advanced tools
Comparing version 0.6.0 to 0.7.0
180
index.js
var transit = require('transit-js'); | ||
var Immutable = require('immutable'); | ||
function recordName(record) { | ||
/* eslint no-underscore-dangle: 0 */ | ||
return record._name || record.constructor.name || 'Record'; | ||
} | ||
function createReader(recordMap, missingRecordHandler) { | ||
function createReader(handlers) { | ||
return transit.reader('json', { | ||
@@ -23,40 +18,53 @@ mapBuilder: { | ||
}, | ||
handlers: { | ||
iM: function(v) { | ||
var m = Immutable.Map().asMutable(); | ||
for (var i = 0; i < v.length; i += 2) { | ||
m = m.set(v[i], v[i + 1]); | ||
} | ||
return m.asImmutable(); | ||
}, | ||
iOM: function(v) { | ||
var m = Immutable.OrderedMap().asMutable(); | ||
for (var i = 0; i < v.length; i += 2) { | ||
m = m.set(v[i], v[i + 1]); | ||
} | ||
return m.asImmutable(); | ||
}, | ||
iL: function(v) { | ||
return Immutable.List(v); | ||
}, | ||
iS: function(v) { | ||
return Immutable.Set(v); | ||
}, | ||
iOS: function(v) { | ||
return Immutable.OrderedSet(v); | ||
}, | ||
iR: function(v) { | ||
var RecordType = recordMap[v.n]; | ||
if (!RecordType) { | ||
return missingRecordHandler(v.n, v.v); | ||
} | ||
handlers: handlers | ||
}); | ||
} | ||
return new RecordType(v.v); | ||
function createReaderHandlers(extras, recordMap, missingRecordHandler) { | ||
var handlers = { | ||
iM: function(v) { | ||
var m = Immutable.Map().asMutable(); | ||
for (var i = 0; i < v.length; i += 2) { | ||
m = m.set(v[i], v[i + 1]); | ||
} | ||
return m.asImmutable(); | ||
}, | ||
iOM: function(v) { | ||
var m = Immutable.OrderedMap().asMutable(); | ||
for (var i = 0; i < v.length; i += 2) { | ||
m = m.set(v[i], v[i + 1]); | ||
} | ||
return m.asImmutable(); | ||
}, | ||
iL: function(v) { | ||
return Immutable.List(v); | ||
}, | ||
iS: function(v) { | ||
return Immutable.Set(v); | ||
}, | ||
iOS: function(v) { | ||
return Immutable.OrderedSet(v); | ||
}, | ||
iR: function(v) { | ||
var RecordType = recordMap[v.n]; | ||
if (!RecordType) { | ||
return missingRecordHandler(v.n, v.v); | ||
} | ||
return new RecordType(v.v); | ||
} | ||
}; | ||
extras.forEach(function(extra) { | ||
handlers[extra.tag] = extra.read; | ||
}); | ||
return handlers; | ||
} | ||
function createWriter(handlers) { | ||
return transit.writer('json', { | ||
handlers: handlers | ||
}); | ||
} | ||
function createWriter(recordMap, predicate) { | ||
function createWriterHandlers(extras, recordMap, predicate) { | ||
function mapSerializer(m) { | ||
@@ -147,7 +155,45 @@ var i = 0; | ||
return transit.writer('json', { | ||
handlers: handlers | ||
extras.forEach(function(extra) { | ||
handlers.set(extra.class, transit.makeWriteHandler({ | ||
tag: function() { return extra.tag; }, | ||
rep: extra.write | ||
})); | ||
}); | ||
return handlers; | ||
} | ||
function validateExtras(extras) { | ||
if (!Array.isArray(extras)) { | ||
invalidExtras(extras, "Expected array of handlers, got %j"); | ||
} | ||
extras.forEach(function(extra) { | ||
if (typeof extra.tag !== "string") { | ||
invalidExtras(extra, | ||
"Expected %j to have property 'tag' which is a string"); | ||
} | ||
if (typeof extra.class !== "function") { | ||
invalidExtras(extra, | ||
"Expected %j to have property 'class' which is a constructor function"); | ||
} | ||
if (typeof extra.write !== "function") { | ||
invalidExtras(extra, | ||
"Expected %j to have property 'write' which is a function"); | ||
} | ||
if (typeof extra.read !== "function") { | ||
invalidExtras(extra, | ||
"Expected %j to have property 'write' which is a function"); | ||
} | ||
}); | ||
} | ||
function invalidExtras(data, msg) { | ||
var json = JSON.stringify(data); | ||
throw new Error(msg.replace("%j", json)); | ||
} | ||
function recordName(record) { | ||
/* eslint no-underscore-dangle: 0 */ | ||
return record._name || record.constructor.name || 'Record'; | ||
} | ||
function makeRecordHandler(name) { | ||
@@ -194,11 +240,6 @@ return transit.makeWriteHandler({ | ||
function createInstance(options) { | ||
var records = options.records || {}; | ||
var filter = options.filter || false; | ||
var missingRecordFn = options.missingRecordHandler | ||
|| defaultMissingRecordHandler; | ||
function createInstanceFromHandlers(handlers) { | ||
var reader = createReader(handlers.read); | ||
var writer = createWriter(handlers.write); | ||
var reader = createReader(records, missingRecordFn); | ||
var writer = createWriter(records, filter); | ||
return { | ||
@@ -211,12 +252,48 @@ toJSON: function toJSON(data) { | ||
}, | ||
withExtraHandlers: function(extra) { | ||
return createInstanceFromHandlers(handlers.withExtraHandlers(extra)); | ||
}, | ||
withFilter: function(predicate) { | ||
return createInstance({ | ||
return createInstanceFromHandlers(handlers.withFilter(predicate)); | ||
}, | ||
withRecords: function(recordClasses, missingRecordHandler) { | ||
return createInstanceFromHandlers( | ||
handlers.withRecords(recordClasses, missingRecordHandler) | ||
); | ||
} | ||
}; | ||
} | ||
function createHandlers(options) { | ||
var records = options.records || {}; | ||
var filter = options.filter || false; | ||
var missingRecordFn = options.missingRecordHandler | ||
|| defaultMissingRecordHandler; | ||
var extras = options.extras || []; | ||
return { | ||
read: createReaderHandlers(extras, records, missingRecordFn), | ||
write: createWriterHandlers(extras, records, filter), | ||
withExtraHandlers: function(moreExtras) { | ||
validateExtras(moreExtras); | ||
return createHandlers({ | ||
extras: extras.concat(moreExtras), | ||
records: records, | ||
filter: predicate, | ||
filter: filter, | ||
missingRecordHandler: missingRecordFn | ||
}); | ||
}, | ||
withFilter: function(newFilter) { | ||
return createHandlers({ | ||
extras: extras, | ||
records: records, | ||
filter: newFilter, | ||
missingRecordHandler: missingRecordFn | ||
}); | ||
}, | ||
withRecords: function(recordClasses, missingRecordHandler) { | ||
var recordMap = buildRecordMap(recordClasses); | ||
return createInstance({ | ||
return createHandlers({ | ||
extras: extras, | ||
records: recordMap, | ||
@@ -230,2 +307,3 @@ filter: filter, | ||
module.exports = createInstance({}); | ||
module.exports = createInstanceFromHandlers(createHandlers({})); | ||
module.exports.handlers = createHandlers({}); |
{ | ||
"name": "transit-immutable-js", | ||
"version": "0.6.0", | ||
"version": "0.7.0", | ||
"description": "Transit serialisation for Immutable.js", | ||
@@ -5,0 +5,0 @@ "main": "index.js", |
@@ -15,3 +15,3 @@ # transit-immutable-js | ||
You must also be using `immutable` for this to be any use. | ||
You must also be using `immutable` for this to be of any use. | ||
@@ -54,2 +54,15 @@ I have chosen to apply very broad npm peerDependencies for simplicity, please check that the versions you have pulled in actually work. | ||
### Usage with transit directly | ||
As well as the nice friendly wrapped API, the internal handlers are exposed in | ||
case you need to work directly with the `transit-js` API. | ||
```js | ||
var transitJS = require('transit-js'); | ||
var handlers = require('transit-immutable-js').handlers; | ||
var reader = transitJS.reader('json', {handlers: handlers.read}); | ||
var writer = transitJS.writer('json-verbose', {handlers: handlers.write}); | ||
``` | ||
## API | ||
@@ -59,3 +72,3 @@ | ||
Convert an immutable object into a JSON representation | ||
Convert an immutable object into a JSON representation ([XSS Warning](#xss-warning)) | ||
@@ -66,5 +79,26 @@ ### `transit.fromJSON(string) => object` | ||
> The `withXXX` methods can be combined as desired. | ||
### `transit.handlers.read` `object` | ||
A mapping of tags to decoding functions which can be used to create a transit reader directly. | ||
### `transit.handlers.write` `transit.map` | ||
A mapping of type constructors to encoding functions which can be used to create a transit writer directly. | ||
**The various `withXXX` methods can be combined as desired by chaining them together.** | ||
### `transit.withExtraHandlers(Array handlers) => transit` | ||
> Also `transit.handlers.withExtraHandlers(Array handlers) => handlers` | ||
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: | ||
* `tag` *string* - a unique identifier for this type that will be used in the serialised output | ||
* `class` *function* - a constructor function that can be used to identify the type via an `instanceof` check | ||
* `write` *function(value)* - a function which will receive an instance of your type, and is expected to create some serialisable representation of it | ||
* `read` *function(rep)* - a function which will receive the serialisable representation, and is expected to create a new instance from it | ||
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. | ||
### `transit.withFilter(function) => transit` | ||
> Also `transit.handlers.withFilter(function) => handlers` | ||
@@ -74,2 +108,3 @@ 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. | ||
### `transit.withRecords(Array recordClasses, missingRecordHandler = null) => transit` | ||
> Also `transit.handlers.withRecords(Array recordClasses, missingRecordHandler = null) => handlers` | ||
@@ -80,2 +115,3 @@ 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`. | ||
## Example `Record` Usage: | ||
@@ -124,1 +160,7 @@ | ||
``` | ||
## XSS Warning | ||
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. | ||
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) |
186
test/test.js
/* eslint-env mocha */ | ||
var chai = require('chai'); | ||
chai.use(require('chai-immutable')); | ||
var expect = chai.expect; | ||
var shared = require("./shared"); | ||
var expect = shared.expect; | ||
var samples = shared.samples; | ||
var expectImmutableEqual = shared.expectImmutableEqual; | ||
var expectNotImmutableEqual = shared.expectNotImmutableEqual; | ||
var Immutable = require('immutable'); | ||
@@ -9,78 +13,2 @@ | ||
var samples = Immutable.Map({ | ||
"Immutable": Immutable.Map({ | ||
"Maps": Immutable.Map({"abc": "def"}), | ||
"Maps with numeric keys": Immutable.Map().set(1, 2), | ||
"Maps in Maps": Immutable.Map() | ||
.set(1, Immutable.Map([['X', 'Y'], ['A', 'B']])) | ||
.set(2, Immutable.Map({a: 1, b: 2, c: 3})), | ||
"Lists": Immutable.List.of(1, 2, 3, 4, 5), | ||
"Long Lists": Immutable.Range(0, 100).toList(), | ||
"Lists in Maps": Immutable.Map().set( | ||
Immutable.List.of(1, 2), | ||
Immutable.List.of(1, 2, 3, 4, 5) | ||
), | ||
"Sets": Immutable.Set.of(1, 2, 3, 3), | ||
"OrderedSets": Immutable.OrderedSet.of(1, 4, 3, 3), | ||
"Ordered Maps": Immutable.OrderedMap() | ||
.set(2, 'a') | ||
.set(3, 'b') | ||
.set(1, 'c') | ||
}), | ||
JS: Immutable.Map({ | ||
"array": [1, 2, 3, 4, 5], | ||
"array of arrays": [ | ||
[1, 2, 3], | ||
[4, 5, 6], | ||
[7, 8, 9, 10] | ||
], | ||
"array of immutables": [ | ||
Immutable.Map({1: 2}), | ||
Immutable.List.of(1, 2, 3) | ||
], | ||
"object": { | ||
a: 1, | ||
b: 2 | ||
}, | ||
"object of immutables": { | ||
a: Immutable.Map({1: 2}), | ||
b: Immutable.Map({3: 4}) | ||
} | ||
}) | ||
}); | ||
// This is a hack because records and maps are considered equivalent by | ||
// immutable. | ||
// https://github.com/astorije/chai-immutable/issues/37 | ||
function expectImmutableEqual(r1, r2) { | ||
expect(r1).to.eql(r2); | ||
expect(r1.toString()).to.eql(r2.toString()); | ||
} | ||
function expectNotImmutableEqual(r1, r2) { | ||
try { | ||
expectImmutableEqual(r1, r2); | ||
} catch (ex) { | ||
return true; | ||
} | ||
throw new chai.AssertionError('Expected ' + r1 + ' to differ from ' + r2); | ||
} | ||
describe('transit', function() { | ||
@@ -392,2 +320,102 @@ samples.get('Immutable').forEach(function(data, desc) { | ||
describe("withExtraHandlers", function() { | ||
function Point2d(x, y) { this.x = x; this.y = y; } | ||
function Point3d(x, y, z) { this.x = x; this.y = y; this.z = z; } | ||
var transitX; | ||
before(function() { | ||
transitX = transit.withExtraHandlers([ | ||
{ | ||
tag: "2d", class: Point2d, | ||
write: function(val) { return [val.x, val.y]; }, | ||
read: function(rep) { return new Point2d(rep[0], rep[1]); } | ||
}, | ||
{ | ||
tag: "3d", class: Point3d, | ||
write: function(val) { return {x: val.x, y: val.y, z: val.z}; }, | ||
read: function(rep) { return new Point3d(rep.x, rep.y, rep.z); } | ||
} | ||
]); | ||
}); | ||
var value = Immutable.Map.of( | ||
123, new Point2d(3, 5), | ||
"co-ords", Immutable.List.of(new Point3d(1, 2, 3), new Point3d(4, 5, 6)) | ||
); | ||
it('should encode into json', function() { | ||
var json = transitX.toJSON(value); | ||
expect(json).to.be.a('string'); | ||
expect(JSON.parse(json)).to.not.eql(null); | ||
}); | ||
it('should round-trip', function() { | ||
var roundTrip = transitX.fromJSON(transitX.toJSON(value)); | ||
expect(roundTrip).to.be.an.instanceof(Immutable.Map); | ||
var point2 = roundTrip.get(123); | ||
expect(point2).to.be.an.instanceof(Point2d); | ||
expect(point2).to.have.property("x", 3); | ||
expect(point2).to.have.property("y", 5); | ||
var point3a = roundTrip.getIn(["co-ords", 0]); | ||
expect(point3a).to.be.an.instanceof(Point3d); | ||
expect(point3a).to.have.property("x", 1); | ||
expect(point3a).to.have.property("y", 2); | ||
expect(point3a).to.have.property("z", 3); | ||
var point3b = roundTrip.getIn(["co-ords", 1]); | ||
expect(point3b).to.be.an.instanceof(Point3d); | ||
expect(point3b).to.have.property("x", 4); | ||
expect(point3b).to.have.property("y", 5); | ||
expect(point3b).to.have.property("z", 6); | ||
}); | ||
describe("argument checking", function() { | ||
function makeExtra(override) { | ||
function ABC() {} | ||
var extra = { | ||
tag: "abc", class: ABC, | ||
read: function(rep) { return ABC(rep); }, | ||
write: function(v) { return v * 0; } | ||
}; | ||
Object.keys(override || {}).forEach(function(key) { | ||
extra[key] = override[key]; | ||
}); | ||
return extra; | ||
} | ||
it("should check for non-array", function() { | ||
expect(function() { | ||
transit.withExtraHandlers({}); | ||
}).to.throw(); | ||
}); | ||
it("should check for string tag", function() { | ||
expect(function() { | ||
transit.withExtraHandlers([ | ||
makeExtra(), | ||
makeExtra({tag: 123}) | ||
]); | ||
}).to.throw(); | ||
}); | ||
it("should check for class function", function() { | ||
expect(function() { | ||
transit.withExtraHandlers([ | ||
makeExtra({class: "blah"}) | ||
]); | ||
}).to.throw(); | ||
}); | ||
it("should check for write function", function() { | ||
expect(function() { | ||
transit.withExtraHandlers([ | ||
makeExtra({write: [1, 2, 3]}) | ||
]); | ||
}).to.throw(); | ||
}); | ||
it("should check for read function", function() { | ||
expect(function() { | ||
transit.withExtraHandlers([ | ||
makeExtra({read: {nope: "not this"}}) | ||
]); | ||
}).to.throw(); | ||
}); | ||
}); | ||
}); | ||
describe('Unknown Input', function() { | ||
@@ -394,0 +422,0 @@ it('fails when an unrecognized object is passed', function() { |
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
35717
13
774
160