serialize-json
Version:
A serialize algorithm for JSON
201 lines (182 loc) • 4.71 kB
JavaScript
'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;