'use strict'; const debug = require('debug')('serialize-json#JSONEncoder'); const is = require('is-type-of'); const utility = require('utility'); const REG_STR_REPLACER = /[\+ \|\^\%]/g; const ENCODER_REPLACER = { ' ': '+', '+': '%2B', '|': '%7C', '^': '%5E', '%': '%25', }; const TOKEN_TRUE = -1; const TOKEN_FALSE = -2; const TOKEN_NULL = -3; const TOKEN_EMPTY_STRING = -4; const TOKEN_UNDEFINED = -5; class JSONEncoder { constructor() { this.dictionary = null; } encode(json) { this.dictionary = { strings: [], integers: [], floats: [], dates: [], }; const ast = this._buildAst(json); let packed = this.dictionary.strings.join('|'); packed += `^${this.dictionary.integers.join('|')}`; packed += `^${this.dictionary.floats.join('|')}`; packed += `^${this.dictionary.dates.join('|')}`; packed += `^${this._pack(ast)}`; debug('pack the json => %s', packed); return Buffer.from(packed); } _pack(ast) { if (is.array(ast)) { let packed = ast.shift(); for (const item of ast) { packed += `${this._pack(item)}|`; } return (packed[packed.length - 1] === '|' ? packed.slice(0, -1) : packed) + ']'; } const type = ast.type; const index = ast.index; const dictionary = this.dictionary; const strLen = dictionary.strings.length; const intLen = dictionary.integers.length; const floatLen = dictionary.floats.length; switch (type) { case 'string': return this._base10To36(index); case 'integer': return this._base10To36(strLen + index); case 'float': return this._base10To36(strLen + intLen + index); case 'date': return this._base10To36(strLen + intLen + floatLen + index); default: return this._base10To36(index); } } _encodeString(str) { return str.replace(REG_STR_REPLACER, a => ENCODER_REPLACER[a]); } _base10To36(num) { return num.toString(36).toUpperCase(); } _dateTo36(date) { return this._base10To36(date.getTime()); } _buildStringAst(str) { const dictionary = this.dictionary; if (str === '') { return { type: 'empty', index: TOKEN_EMPTY_STRING, }; } const data = this._encodeString(str); return { type: 'string', index: dictionary.strings.push(data) - 1, }; } _buildNumberAst(num) { const dictionary = this.dictionary; // integer if (num % 1 === 0) { const data = this._base10To36(num); return { type: 'integer', index: dictionary.integers.push(data) - 1, }; } // float return { type: 'float', index: dictionary.floats.push(num) - 1, }; } _buildObjectAst(obj) { if (obj === null) { return { type: 'null', index: TOKEN_NULL, }; } if (is.date(obj)) { const dictionary = this.dictionary; const data = this._dateTo36(obj); return { type: 'date', index: dictionary.dates.push(data) - 1, }; } let ast; if (is.array(obj)) { ast = [ '@' ]; for (const item of obj) { ast.push(this._buildAst(item)); } return ast; } if (is.buffer(obj)) { ast = [ '*' ]; for (const item of obj.values()) { ast.push(this._buildAst(item)); } return ast; } if (is.error(obj)) { ast = [ '#' ]; ast.push(this._buildAst('message')); ast.push(this._buildAst(obj.message)); ast.push(this._buildAst('stack')); ast.push(this._buildAst(obj.stack)); } else { ast = [ '$' ]; } for (const key in obj) { // support object without prototype, like: Object.create(null) if (!utility.has(obj, key)) { continue; } ast.push(this._buildAst(key)); ast.push(this._buildAst(obj[key])); } return ast; } _buildAst(item) { const type = typeof item; debug('calling buildAst with type: %s and data: %j', type, item); switch (type) { case 'string': return this._buildStringAst(item); case 'number': return this._buildNumberAst(item); case 'boolean': return { type: 'boolean', index: item ? TOKEN_TRUE : TOKEN_FALSE, }; case 'undefined': return { type: 'undefined', index: TOKEN_UNDEFINED, }; case 'object': return this._buildObjectAst(item); default: debug('unsupported type: %s, return null', type); return { type: 'null', index: TOKEN_NULL, }; } } } module.exports = JSONEncoder;