Skip to content

Commit

Permalink
Merge 6f29b4a into a2bb3b6
Browse files Browse the repository at this point in the history
  • Loading branch information
glenjamin committed Dec 13, 2016
2 parents a2bb3b6 + 6f29b4a commit 8dffafe
Show file tree
Hide file tree
Showing 5 changed files with 269 additions and 128 deletions.
27 changes: 26 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,19 @@ console.log(transit.fromJSON(transit.toJSON(obj)));
// array: [ 'javascript', 4, 'lyfe' ] }
```

### 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

### `transit.toJSON(object) => string`
Expand All @@ -60,18 +73,29 @@ Convert an immutable object into a JSON representation ([XSS Warning](#xss-warni

Convert a JSON representation back into an immutable 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.withFilter(function) => transit`
> Also `transit.handlers.withFilter(function) => handlers`
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`
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`.

`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.


## Example `Record` Usage:

```js
Expand Down Expand Up @@ -117,6 +141,7 @@ var decodedResult = recordTransitEmpty.fromJSON(encodedJSON); // returns new Bar
```

## 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)
119 changes: 71 additions & 48 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ function recordName(record) {
return record._name || record.constructor.name || 'Record';
}

function createReader(recordMap, missingRecordHandler) {
function createReader(handlers) {
return transit.reader('json', {
mapBuilder: {
init: function() {
Expand All @@ -20,44 +20,53 @@ function createReader(recordMap, missingRecordHandler) {
return m;
}
},
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(recordMap, missingRecordHandler) {
return {
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);
}
});
};
}

function createWriter(handlers) {
return transit.writer('json', {
handlers: handlers
});
}

function createWriter(recordMap, predicate) {
function createWriterHandlers(recordMap, predicate) {
function mapSerializer(m) {
var i = 0;
if (predicate) {
Expand Down Expand Up @@ -143,9 +152,7 @@ function createWriter(recordMap, predicate) {
handlers.set(recordMap[name], makeRecordHandler(name, predicate));
});

return transit.writer('json', {
handlers: handlers
});
return handlers;
}

function makeRecordHandler(name) {
Expand Down Expand Up @@ -189,14 +196,9 @@ function defaultMissingRecordHandler(recName) {
throw new Error(msg);
}

function createInstance(options) {
var records = options.records || {};
var filter = options.filter || false;
var missingRecordFn = options.missingRecordHandler
|| defaultMissingRecordHandler;

var reader = createReader(records, missingRecordFn);
var writer = createWriter(records, filter);
function createInstanceFromHandlers(handlers) {
var reader = createReader(handlers.read);
var writer = createWriter(handlers.write);

return {
toJSON: function toJSON(data) {
Expand All @@ -206,15 +208,35 @@ function createInstance(options) {
return reader.read(json);
},
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;

return {
read: createReaderHandlers(records, missingRecordFn),
write: createWriterHandlers(records, filter),
withFilter: function(newFilter) {
return createHandlers({
records: records,
filter: predicate,
filter: newFilter,
missingRecordHandler: missingRecordFn
});
},
withRecords: function(recordClasses, missingRecordHandler) {
var recordMap = buildRecordMap(recordClasses);
return createInstance({
return createHandlers({
records: recordMap,
filter: filter,
missingRecordHandler: missingRecordHandler
Expand All @@ -223,4 +245,5 @@ function createInstance(options) {
};
}

module.exports = createInstance({});
module.exports = createInstanceFromHandlers(createHandlers({}));
module.exports.handlers = createHandlers({});
80 changes: 80 additions & 0 deletions test/handlers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/* eslint-env mocha */
var shared = require("./shared");

var expect = shared.expect;
var samples = shared.samples;
var expectImmutableEqual = shared.expectImmutableEqual;

var transitJS = require('transit-js');

var handlers = require('../').handlers;

var reader = transitJS.reader('json', { handlers: handlers.read });
var writer = transitJS.writer('json', { handlers: handlers.write });

describe("direct handlers usage", function() {

samples.get('Immutable').forEach(function(data, desc) {
describe(desc + " - " + data.inspect(), function() {
it('should encode to JSON', function() {
var json = writer.write(data);
expect(json).to.be.a('string');
expect(JSON.parse(json)).to.not.eql(null);
});
it('should round-trip', function() {
var roundTrip = reader.read(writer.write(data));
expect(roundTrip).to.be.an('object');
expectImmutableEqual(roundTrip, data);
expect(roundTrip).to.be.an.instanceOf(data.constructor);
});
});
});

describe("extending handlers", function() {
function Blah(x) { this.x = x; }
var extendedRead = {
blah: function(v) { return new Blah(v); }
};
Object.keys(handlers.read).forEach(function(tag) {
extendedRead[tag] = handlers.read[tag];
});
var extendedWrite = handlers.write.clone();
extendedWrite.set(Blah, transitJS.makeWriteHandler({
tag: function() { return 'blah'; },
rep: function(v) { return v.x; }
}));

var readerX = transitJS.reader('json', {handlers: extendedRead});
var writerX = transitJS.writer('json', {handlers: extendedWrite});

describe("extended type", function() {
it('should encode to JSON', function() {
var json = writerX.write(new Blah(123));
expect(json).to.be.a('string');
expect(JSON.parse(json)).to.not.eql(null);
});
it('should round-trip', function() {
var roundTrip = readerX.read(writerX.write(new Blah(456)));
expect(roundTrip).to.be.an.instanceOf(Blah);
expect(roundTrip).to.have.property("x", 456);
});
});

samples.get('Immutable').forEach(function(data, desc) {
describe(desc + " - " + data.inspect(), function() {
it('should encode to JSON', function() {
var json = writerX.write(data);
expect(json).to.be.a('string');
expect(JSON.parse(json)).to.not.eql(null);
});
it('should round-trip', function() {
var roundTrip = readerX.read(writerX.write(data));
expect(roundTrip).to.be.an('object');
expectImmutableEqual(roundTrip, data);
expect(roundTrip).to.be.an.instanceOf(data.constructor);
});
});
});

});
});
85 changes: 85 additions & 0 deletions test/shared.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
var chai = require('chai');
var Immutable = require('immutable');

chai.use(require('chai-immutable'));
var expect = chai.expect;
exports.expect = expect;

exports.samples = Immutable.Map({

"Immutable": Immutable.Map({

"Maps": Immutable.Map({"abc": "def\nghi"}),

"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());
}
exports.expectImmutableEqual = expectImmutableEqual;

function expectNotImmutableEqual(r1, r2) {
try {
expectImmutableEqual(r1, r2);
} catch (ex) {
return true;
}
throw new chai.AssertionError('Expected ' + r1 + ' to differ from ' + r2);
}
exports.expectNotImmutableEqual = expectNotImmutableEqual;

0 comments on commit 8dffafe

Please sign in to comment.