diff --git a/index.js b/index.js index 71911ab..9f52288 100644 --- a/index.js +++ b/index.js @@ -16,3 +16,4 @@ exports.createPromiseCallback = require('./lib/utils').createPromiseCallback; // KeyValue helpers exports.ModelKeyComposer = require('./lib/model-key-composer'); +exports.BinaryPacker = require('./lib/binary-packer'); diff --git a/lib/binary-packer.js b/lib/binary-packer.js new file mode 100644 index 0000000..71d175e --- /dev/null +++ b/lib/binary-packer.js @@ -0,0 +1,80 @@ +// Copyright IBM Corp. 2014,2016. All Rights Reserved. +// Node module: loopback-connector +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +'use strict'; + +var createPromiseCallback = require('./utils').createPromiseCallback; +var msgpack = require('msgpack5'); + +module.exports = BinaryPacker; + +/** + * Create a new Packer instance that can be used to convert between JavaScript + * objects and a binary representation in a Buffer. + * + * Compared to JSON, this encoding preserves the following JavaScript types: + * - Date + */ +function BinaryPacker() { + this._packer = msgpack({ forceFloat64: true }); + this._packer.register(1, Date, encodeDate, decodeDate); +} + +/** + * Encode the provided value to a `Buffer`. + * + * @param {*} value Any value (string, number, object) + * @callback {Function} cb The callback to receive the parsed result. + * @param {Error} err + * @param {Buffer} data The encoded value + * @promise + */ +BinaryPacker.prototype.encode = function(value, cb) { + cb = cb || createPromiseCallback(); + try { + // msgpack5 returns https://www.npmjs.com/package/bl instead of Buffer + // use .slice() to convert to a Buffer + var data = this._packer.encode(value).slice(); + setImmediate(function() { + cb(null, data); + }); + } catch (err) { + setImmediate(function() { + cb(err); + }); + } + return cb.promise; +}; + +/** + * Decode the binary value back to a JavaScript value. + * @param {Buffer} binary The binary input. + * @callback {Function} cb The callback to receive the composed value. + * @param {Error} err + * @param {*} value Decoded value. + * @promise + */ +BinaryPacker.prototype.decode = function(binary, cb) { + cb = cb || createPromiseCallback(); + try { + var value = this._packer.decode(binary); + setImmediate(function() { + cb(null, value); + }); + } catch (err) { + setImmediate(function() { + cb(err); + }); + } + return cb.promise; +}; + +function encodeDate(obj) { + return new Buffer(obj.toISOString(), 'utf8'); +} + +function decodeDate(buf) { + return new Date(buf.toString('utf8')); +} diff --git a/package.json b/package.json index b1720e7..b156c7c 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "async": "^1.0.0", "bluebird": "^3.4.6", "debug": "^2.2.0", + "msgpack5": "^3.4.1", "strong-globalize": "^2.5.8" }, "devDependencies": { diff --git a/test/binary-packer.test.js b/test/binary-packer.test.js new file mode 100644 index 0000000..c2e6c32 --- /dev/null +++ b/test/binary-packer.test.js @@ -0,0 +1,72 @@ +// Copyright IBM Corp. 2016. All Rights Reserved. +// Node module: loopback-connector +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +'use strict'; + +var BinaryPacker = require('../lib/binary-packer'); +var expect = require('chai').expect; + +describe('BinaryPacker', function() { + var packer; + + beforeEach(function createPacker() { + packer = new BinaryPacker(); + }); + + describe('encode()', function() { + it('supports invocation with a callback', function(done) { + packer.encode('a-value', done); + }); + }); + + describe('decode()', function() { + it('supports invocation with a callback', function(done) { + packer.encode('a-value', function(err, binary) { + if (err) return done(err); + packer.decode(binary, function(err, result) { + if (err) return done(err); + expect(result).to.eql('a-value'); + done(); + }); + }); + }); + }); + + describe('roundtrip', function() { + var TEST_CASES = { + String: 'a-value', + Object: { a: 1, b: 2 }, + Buffer: new Buffer([1, 2, 3]), + Date: new Date('2016-08-03T11:53:03.470Z'), + Integer: 12345, + Float: 12.345, + Boolean: false, + }; + + Object.keys(TEST_CASES).forEach(function(tc) { + it('works for ' + tc + ' values', function() { + var value = TEST_CASES[tc]; + return encodeAndDecode(value) + .then(function(result) { + expect(result).to.eql(value); + }); + }); + }); + + it('works for nested properties', function() { + return encodeAndDecode(TEST_CASES) + .then(function(result) { + expect(result).to.eql(TEST_CASES); + }); + }); + + function encodeAndDecode(value) { + return packer.encode(value) + .then(function(binary) { + return packer.decode(binary); + }); + } + }); +});