UNPKG

js-binary

Version:

Encode/decode to a custom binary format, much more compact and faster than JSON/BSON

273 lines (243 loc) 6.78 kB
'use strict' var types = require('./types'), Data, ReadState, Field /** * Create a type, given the format. The format can be either: * * A basic type, one of: * `'uint', 'int', 'float', 'string', 'Buffer', 'boolean', 'json', 'oid', 'regex', 'date'` * * A compound type: an object, like: * `{a: 'int', b: ['int'], c: [{'d?': 'string'}]}` * In the example above, 'b' is a an array of integers, 'd' is an optional field * * An array of values of the same type: * `['int']` * @class * @param {string|Object|Array} type */ function Type(type) { /** * @member {Type.TYPE} Type#type */ /** * Defined fields in an `OBJECT` type * @member {?Array<Field>} Type#fields */ /** * Elements type for an `ARRAY` type * @member {?Type} Type#subType */ if (typeof type === 'string') { if (type in Type.TYPE && type !== Type.TYPE.ARRAY && type !== Type.TYPE.OBJECT) { throw new TypeError('Unknown basic type: ' + type) } this.type = type } else if (Array.isArray(type)) { if (type.length !== 1) { throw new TypeError('Invalid array type, it must have exactly one element') } this.type = Type.TYPE.ARRAY this.subType = new Type(type[0]) } else { if (!type || typeof type !== 'object') { throw new TypeError('Invalid type: ' + type) } this.type = Type.TYPE.OBJECT this.fields = Object.keys(type).map(function (name) { return new Field(name, type[name]) }) } } module.exports = Type Data = require('./Data') ReadState = require('./ReadState') Field = require('./Field') /** * All possible types * @enum {string} */ Type.TYPE = { UINT: 'uint', INT: 'int', FLOAT: 'float', STRING: 'string', BUFFER: 'Buffer', BOOLEAN: 'boolean', JSON: 'json', OID: 'oid', REGEX: 'regex', DATE: 'date', ARRAY: '[array]', OBJECT: '{object}' } /** * Expose all scalar types (see types.js) * @property {Object<Function>} */ Type.types = Type.prototype.types = types /** * @param {*} value * @return {Buffer} * @throws if the value is invalid */ Type.prototype.encode = function (value) { var data = new Data this.write(value, data, '') return data.toBuffer() } /** * @param {Buffer} buffer * @return {*} * @throws if fails */ Type.prototype.decode = function (buffer) { return this.read(new ReadState(buffer)) } /** * @param {*} value * @param {Data} data * @param {string} path * @throws if the value is invalid */ Type.prototype.write = function (value, data, path) { var i, field, subpath, subValue, len if (this.type === Type.TYPE.ARRAY) { // Array field return this._writeArray(value, data, path, this.subType) } else if (this.type !== Type.TYPE.OBJECT) { // Simple type return types[this.type].write(value, data, path) } // Check for object type if (!value || typeof value !== 'object') { throw new TypeError('Expected an object at ' + path) } // Write each field for (i = 0, len = this.fields.length; i < len; i++) { field = this.fields[i] subpath = path ? path + '.' + field.name : field.name subValue = value[field.name] if (field.optional) { // Add 'presence' flag if (subValue === undefined || subValue === null) { types.boolean.write(false, data) continue } else { types.boolean.write(true, data) } } if (!field.array) { // Scalar field field.type.write(subValue, data, subpath) continue } // Array field this._writeArray(subValue, data, subpath, field.type) } } /** * @param {*} value * @param {Data} data * @param {string} path * @param {Type} type * @throws if the value is invalid * @private */ Type.prototype._writeArray = function (value, data, path, type) { var i, len if (!Array.isArray(value)) { throw new TypeError('Expected an Array at ' + path) } len = value.length types.uint.write(len, data) for (i = 0; i < len; i++) { type.write(value[i], data, path + '.' + i) } } /** * This funciton will be executed only the first time * After that, we'll compile the read routine and add it directly to the instance * @param {ReadState} state * @return {*} * @throws if fails */ Type.prototype.read = function (state) { this.read = this._compileRead() return this.read(state) } /** * Return a signature for this type. Two types that resolve to the same hash can be said as equivalents * @return {Buffer} */ Type.prototype.getHash = function () { var hash = new Data hashType(this, false, false) return hash.toBuffer() /** * @param {Type} type * @param {boolean} array * @param {boolean} optional */ function hashType(type, array, optional) { // Write type (first char + flags) // AOxx xxxx hash.writeUInt8((type.type.charCodeAt(0) & 0x3f) | (array ? 0x80 : 0) | (optional ? 0x40 : 0)) if (type.type === Type.TYPE.ARRAY) { hashType(type.subType, false, false) } else if (type.type === Type.TYPE.OBJECT) { types.uint.write(type.fields.length, hash) type.fields.forEach(function (field) { hashType(field.type, field.array, field.optional) }) } } } /** * Compile the decode method for this object * @return {function(ReadState):*} * @private */ Type.prototype._compileRead = function () { if (this.type !== Type.TYPE.OBJECT && this.type !== Type.TYPE.ARRAY) { // Scalar type // In this case, there is no need to write custom code return types[this.type].read } else if (this.type === Type.TYPE.ARRAY) { return this._readArray.bind(this, this.subType) } // As an example, compiling code to new Type({a:'int', 'b?':['string']}) will result in: // return { // a: this.fields[0].type.read(state), // b: this.types.boolean.read(state) ? this._readArray(state, this.fields[1].type) : undefined // } var code = 'return {' + this.fields.map(function (field, i) { var name = JSON.stringify(field.name), fieldStr = 'this.fields[' + i + ']', readCode, code if (field.array) { readCode = 'this._readArray(' + fieldStr + '.type, state)' } else { readCode = fieldStr + '.type.read(state)' } if (!field.optional) { code = name + ': ' + readCode } else { code = name + ': this.types.boolean.read(state) ? ' + readCode + ' : undefined' } return code }).join(',') + '}' return new Function('state', code) } /** * @param {Type} type * @param {ReadState} state * @return {Array} * @throws - if invalid * @private */ Type.prototype._readArray = function (type, state) { var arr = new Array(types.uint.read(state)), j for (j = 0; j < arr.length; j++) { arr[j] = type.read(state) } return arr }