encoder.js 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200
  1. 'use strict';
  2. const debug = require('debug')('serialize-json#JSONEncoder');
  3. const is = require('is-type-of');
  4. const utility = require('utility');
  5. const REG_STR_REPLACER = /[\+ \|\^\%]/g;
  6. const ENCODER_REPLACER = {
  7. ' ': '+',
  8. '+': '%2B',
  9. '|': '%7C',
  10. '^': '%5E',
  11. '%': '%25',
  12. };
  13. const TOKEN_TRUE = -1;
  14. const TOKEN_FALSE = -2;
  15. const TOKEN_NULL = -3;
  16. const TOKEN_EMPTY_STRING = -4;
  17. const TOKEN_UNDEFINED = -5;
  18. class JSONEncoder {
  19. constructor() {
  20. this.dictionary = null;
  21. }
  22. encode(json) {
  23. this.dictionary = {
  24. strings: [],
  25. integers: [],
  26. floats: [],
  27. dates: [],
  28. };
  29. const ast = this._buildAst(json);
  30. let packed = this.dictionary.strings.join('|');
  31. packed += `^${this.dictionary.integers.join('|')}`;
  32. packed += `^${this.dictionary.floats.join('|')}`;
  33. packed += `^${this.dictionary.dates.join('|')}`;
  34. packed += `^${this._pack(ast)}`;
  35. debug('pack the json => %s', packed);
  36. return Buffer.from(packed);
  37. }
  38. _pack(ast) {
  39. if (is.array(ast)) {
  40. let packed = ast.shift();
  41. for (const item of ast) {
  42. packed += `${this._pack(item)}|`;
  43. }
  44. return (packed[packed.length - 1] === '|' ? packed.slice(0, -1) : packed) + ']';
  45. }
  46. const type = ast.type;
  47. const index = ast.index;
  48. const dictionary = this.dictionary;
  49. const strLen = dictionary.strings.length;
  50. const intLen = dictionary.integers.length;
  51. const floatLen = dictionary.floats.length;
  52. switch (type) {
  53. case 'string':
  54. return this._base10To36(index);
  55. case 'integer':
  56. return this._base10To36(strLen + index);
  57. case 'float':
  58. return this._base10To36(strLen + intLen + index);
  59. case 'date':
  60. return this._base10To36(strLen + intLen + floatLen + index);
  61. default:
  62. return this._base10To36(index);
  63. }
  64. }
  65. _encodeString(str) {
  66. return str.replace(REG_STR_REPLACER, a => ENCODER_REPLACER[a]);
  67. }
  68. _base10To36(num) {
  69. return num.toString(36).toUpperCase();
  70. }
  71. _dateTo36(date) {
  72. return this._base10To36(date.getTime());
  73. }
  74. _buildStringAst(str) {
  75. const dictionary = this.dictionary;
  76. if (str === '') {
  77. return {
  78. type: 'empty',
  79. index: TOKEN_EMPTY_STRING,
  80. };
  81. }
  82. const data = this._encodeString(str);
  83. return {
  84. type: 'string',
  85. index: dictionary.strings.push(data) - 1,
  86. };
  87. }
  88. _buildNumberAst(num) {
  89. const dictionary = this.dictionary;
  90. // integer
  91. if (num % 1 === 0) {
  92. const data = this._base10To36(num);
  93. return {
  94. type: 'integer',
  95. index: dictionary.integers.push(data) - 1,
  96. };
  97. }
  98. // float
  99. return {
  100. type: 'float',
  101. index: dictionary.floats.push(num) - 1,
  102. };
  103. }
  104. _buildObjectAst(obj) {
  105. if (obj === null) {
  106. return {
  107. type: 'null',
  108. index: TOKEN_NULL,
  109. };
  110. }
  111. if (is.date(obj)) {
  112. const dictionary = this.dictionary;
  113. const data = this._dateTo36(obj);
  114. return {
  115. type: 'date',
  116. index: dictionary.dates.push(data) - 1,
  117. };
  118. }
  119. let ast;
  120. if (is.array(obj)) {
  121. ast = [ '@' ];
  122. for (const item of obj) {
  123. ast.push(this._buildAst(item));
  124. }
  125. return ast;
  126. }
  127. if (is.buffer(obj)) {
  128. ast = [ '*' ];
  129. for (const item of obj.values()) {
  130. ast.push(this._buildAst(item));
  131. }
  132. return ast;
  133. }
  134. if (is.error(obj)) {
  135. ast = [ '#' ];
  136. ast.push(this._buildAst('message'));
  137. ast.push(this._buildAst(obj.message));
  138. ast.push(this._buildAst('stack'));
  139. ast.push(this._buildAst(obj.stack));
  140. } else {
  141. ast = [ '$' ];
  142. }
  143. for (const key in obj) {
  144. // support object without prototype, like: Object.create(null)
  145. if (!utility.has(obj, key)) {
  146. continue;
  147. }
  148. ast.push(this._buildAst(key));
  149. ast.push(this._buildAst(obj[key]));
  150. }
  151. return ast;
  152. }
  153. _buildAst(item) {
  154. const type = typeof item;
  155. debug('calling buildAst with type: %s and data: %j', type, item);
  156. switch (type) {
  157. case 'string':
  158. return this._buildStringAst(item);
  159. case 'number':
  160. return this._buildNumberAst(item);
  161. case 'boolean':
  162. return {
  163. type: 'boolean',
  164. index: item ? TOKEN_TRUE : TOKEN_FALSE,
  165. };
  166. case 'undefined':
  167. return {
  168. type: 'undefined',
  169. index: TOKEN_UNDEFINED,
  170. };
  171. case 'object':
  172. return this._buildObjectAst(item);
  173. default:
  174. debug('unsupported type: %s, return null', type);
  175. return {
  176. type: 'null',
  177. index: TOKEN_NULL,
  178. };
  179. }
  180. }
  181. }
  182. module.exports = JSONEncoder;