1 | 'use strict';
|
2 |
|
3 | const isPlainObject = require('lodash.isplainobject');
|
4 | const isArray = require('lodash.isarray');
|
5 | const isString = require('lodash.isstring');
|
6 | const isNumber = require('lodash.isnumber');
|
7 | const isEmpty = require('lodash.isempty');
|
8 | const has = require('lodash.has');
|
9 | const find = require('lodash.find');
|
10 | const toNumber = require('lodash.tonumber');
|
11 | const toString = require('lodash.tostring');
|
12 | const includes = require('lodash.includes');
|
13 | const bson = require('bson');
|
14 | const MinKey = bson.MinKey;
|
15 | const MaxKey = bson.MaxKey;
|
16 | const Long = bson.Long;
|
17 | const Double = bson.Double;
|
18 | const Int32 = bson.Int32;
|
19 | const Decimal128 = bson.Decimal128;
|
20 |
|
21 |
|
22 |
|
23 |
|
24 | const OBJECT = 'Object';
|
25 |
|
26 |
|
27 |
|
28 |
|
29 | const ARRAY = 'Array';
|
30 |
|
31 |
|
32 |
|
33 |
|
34 | const BSON_TYPE = '_bsontype';
|
35 |
|
36 |
|
37 |
|
38 |
|
39 | const MATCH = /\[object (\w+)\]/;
|
40 |
|
41 |
|
42 |
|
43 |
|
44 | const BSON_INT32_MAX = 0x7FFFFFFF;
|
45 |
|
46 |
|
47 |
|
48 |
|
49 | const BSON_INT32_MIN = -0x80000000;
|
50 |
|
51 |
|
52 |
|
53 |
|
54 | const MAX_DBL = Number.MAX_SAFE_INTEGER;
|
55 |
|
56 |
|
57 |
|
58 |
|
59 | const NUMBER_TYPES = [
|
60 | 'Long',
|
61 | 'Int32',
|
62 | 'Double',
|
63 | 'Decimal128'
|
64 | ];
|
65 |
|
66 | function toDate(object) {
|
67 | return new Date(object);
|
68 | }
|
69 |
|
70 | function toMinKey() {
|
71 | return new MinKey();
|
72 | }
|
73 |
|
74 | function toMaxKey() {
|
75 | return new MaxKey();
|
76 | }
|
77 |
|
78 | function toUndefined() {
|
79 | return undefined;
|
80 | }
|
81 |
|
82 | function toNull() {
|
83 | return null;
|
84 | }
|
85 |
|
86 | function toBoolean(object) {
|
87 | if (isString(object)) {
|
88 | if (object.toLowerCase() === 'true') {
|
89 | return true;
|
90 | }
|
91 | return false;
|
92 | }
|
93 | if (object) {
|
94 | return true;
|
95 | }
|
96 | return false;
|
97 | }
|
98 |
|
99 | function toObject(object) {
|
100 | if (isPlainObject(object)) {
|
101 | return object;
|
102 | }
|
103 | return {};
|
104 | }
|
105 |
|
106 | function toArray(object) {
|
107 | if (isArray(object)) {
|
108 | return object;
|
109 | }
|
110 | if (isPlainObject(object)) {
|
111 | return [];
|
112 | }
|
113 | return [ object ];
|
114 | }
|
115 |
|
116 | function toInt32(object) {
|
117 | return new Int32(toNumber(object));
|
118 | }
|
119 |
|
120 | function toInt64(object) {
|
121 | return Long.fromNumber(toNumber(object));
|
122 | }
|
123 |
|
124 | function toDouble(object) {
|
125 | return new Double(toNumber(object));
|
126 | }
|
127 |
|
128 | function toDecimal128(object) {
|
129 | return Decimal128.fromString(String(object));
|
130 | }
|
131 |
|
132 |
|
133 |
|
134 |
|
135 | const CASTERS = {
|
136 | 'Int32': toInt32,
|
137 | 'Int64': toInt64,
|
138 | 'Double': toDouble,
|
139 | 'Decimal128': toDecimal128,
|
140 | 'Date': toDate,
|
141 | 'MinKey': toMinKey,
|
142 | 'MaxKey': toMaxKey,
|
143 | 'Undefined': toUndefined,
|
144 | 'Null': toNull,
|
145 | 'Boolean': toBoolean,
|
146 | 'String': toString,
|
147 | 'Object': toObject,
|
148 | 'Array': toArray
|
149 | };
|
150 |
|
151 |
|
152 |
|
153 |
|
154 | class Test {
|
155 | constructor(tester, types) {
|
156 | this.tester = tester;
|
157 | this.types = types;
|
158 | }
|
159 | }
|
160 |
|
161 | const NUMBER_REGEX = /^-?\d+$/;
|
162 |
|
163 |
|
164 |
|
165 |
|
166 | class Int32Check {
|
167 | test(string) {
|
168 | if (NUMBER_REGEX.test(string)) {
|
169 | var value = toNumber(string);
|
170 | return value >= BSON_INT32_MIN && value <= BSON_INT32_MAX;
|
171 | }
|
172 | return false;
|
173 | }
|
174 | }
|
175 |
|
176 |
|
177 |
|
178 |
|
179 | class Int64Check {
|
180 | test(string) {
|
181 | if (NUMBER_REGEX.test(string)) {
|
182 | return Number.isSafeInteger(toNumber(string));
|
183 | }
|
184 | return false;
|
185 | }
|
186 | }
|
187 |
|
188 |
|
189 |
|
190 |
|
191 | class IntDblCheck {
|
192 | test(string) {
|
193 | if (NUMBER_REGEX.test(string)) {
|
194 | var value = toNumber(string);
|
195 | return value >= -MAX_DBL && value <= MAX_DBL;
|
196 | }
|
197 | return false;
|
198 | }
|
199 | }
|
200 |
|
201 | const DOUBLE_REGEX = /^-?(\d*\.)?\d{1,15}$/;
|
202 |
|
203 |
|
204 |
|
205 |
|
206 | class DoubleCheck {
|
207 | test(string) {
|
208 | if (DOUBLE_REGEX.test(string)) {
|
209 | var value = toNumber(string);
|
210 | return value >= -MAX_DBL && value <= MAX_DBL;
|
211 | }
|
212 | return false;
|
213 | }
|
214 | }
|
215 |
|
216 | var PARSE_STRING_REGEXP = /^(?=\d)(\+|\-)?(\d+|(\d*\.\d*))?(E|e)?([\-\+])?(\d+)?$/;
|
217 | var PARSE_INF_REGEXP = /^(\+|\-)?(Infinity|inf)$/i;
|
218 | var PARSE_NAN_REGEXP = /^(\+|\-)?NaN$/i;
|
219 |
|
220 |
|
221 |
|
222 |
|
223 | class Decimal128Check {
|
224 | test(string) {
|
225 | const stringMatch = string.match(PARSE_STRING_REGEXP);
|
226 | const infMatch = string.match(PARSE_INF_REGEXP);
|
227 | const nanMatch = string.match(PARSE_NAN_REGEXP);
|
228 |
|
229 | const regex = stringMatch || infMatch || nanMatch;
|
230 | const exp = stringMatch && stringMatch[4] && stringMatch[2] === undefined;
|
231 |
|
232 | return string.length !== 0 && regex && !exp;
|
233 | }
|
234 | }
|
235 |
|
236 | const DATE_REGEX = /^(\d{4})-(\d|\d{2})-(\d|\d{2})(T\d{2}\:\d{2}\:\d{2}(\.\d+)?)?$/;
|
237 |
|
238 |
|
239 |
|
240 |
|
241 | class DateCheck {
|
242 | test(string) {
|
243 | if (DATE_REGEX.test(string)) {
|
244 | var date = Date.parse(string);
|
245 | return date ? true : false;
|
246 | }
|
247 | }
|
248 | }
|
249 |
|
250 | const INT32_CHECK = new Int32Check();
|
251 | const INT64_CHECK = new Int64Check();
|
252 | const INT_DBL_CHECK = new IntDblCheck();
|
253 | const DOUBLE_CHECK = new DoubleCheck();
|
254 | const DECIMAL_128_CHECK = new Decimal128Check();
|
255 | const DATE_CHECK = new DateCheck();
|
256 |
|
257 |
|
258 |
|
259 |
|
260 | const STRING_TESTS = [
|
261 | new Test(/^$/, [ 'String', 'Null', 'MinKey', 'MaxKey', 'Object', 'Array' ]),
|
262 | new Test(INT32_CHECK, [ 'Int32', 'Int64', 'Double', 'String', 'Object', 'Array' ]),
|
263 | new Test(INT_DBL_CHECK, [ 'Int64', 'Double', 'String', 'Object', 'Array' ]),
|
264 | new Test(INT64_CHECK, [ 'Int64', 'String', 'Object', 'Array' ]),
|
265 | new Test(DOUBLE_CHECK, [ 'Double', 'String', 'Object', 'Array' ]),
|
266 | new Test(/^(null)$/, [ 'Null', 'String', 'Object', 'Array' ]),
|
267 | new Test(/^(undefined)$/, [ 'Undefined', 'String', 'Object', 'Array' ]),
|
268 | new Test(/^(true|false)$/, [ 'Boolean', 'String', 'Object', 'Array' ]),
|
269 | new Test(/^\/(.*)\/$/, [ 'BSONRegExp', 'String', 'Object', 'Array' ]),
|
270 | new Test(DATE_CHECK, [ 'Date', 'String', 'Object', 'Array' ])
|
271 | ];
|
272 |
|
273 |
|
274 |
|
275 |
|
276 | const HP_STRING_TESTS = [
|
277 | new Test(/^$/, [ 'String', 'Null', 'MinKey', 'MaxKey', 'Object', 'Array' ]),
|
278 | new Test(INT32_CHECK, [ 'Int32', 'Int64', 'Double', 'Decimal128', 'String', 'Object', 'Array' ]),
|
279 | new Test(INT_DBL_CHECK, [ 'Int64', 'Double', 'Decimal128', 'String', 'Object', 'Array' ]),
|
280 | new Test(INT64_CHECK, [ 'Int64', 'Decimal128', 'String', 'Object', 'Array' ]),
|
281 | new Test(DOUBLE_CHECK, [ 'Double', 'Decimal128', 'String', 'Object', 'Array' ]),
|
282 | new Test(DECIMAL_128_CHECK, [ 'Decimal128', 'String', 'Object', 'Array' ]),
|
283 | new Test(/^(null)$/, [ 'Null', 'String', 'Object', 'Array' ]),
|
284 | new Test(/^(undefined)$/, [ 'Undefined', 'String', 'Object', 'Array' ]),
|
285 | new Test(/^(true|false)$/, [ 'Boolean', 'String', 'Object', 'Array' ]),
|
286 | new Test(/^\/(.*)\/$/, [ 'BSONRegExp', 'String', 'Object', 'Array' ]),
|
287 | new Test(DATE_CHECK, [ 'Date', 'String', 'Object', 'Array' ])
|
288 | ];
|
289 |
|
290 |
|
291 |
|
292 |
|
293 |
|
294 |
|
295 |
|
296 |
|
297 | function numberToBsonType(number) {
|
298 | var string = toString(number);
|
299 | if (INT32_CHECK.test(string)) {
|
300 | return 'Int32';
|
301 | } else if (INT64_CHECK.test(string)) {
|
302 | return 'Int64';
|
303 | }
|
304 | return 'Double';
|
305 | }
|
306 |
|
307 |
|
308 |
|
309 |
|
310 | class TypeChecker {
|
311 |
|
312 | |
313 |
|
314 |
|
315 |
|
316 |
|
317 |
|
318 |
|
319 |
|
320 | cast(object, type) {
|
321 | var caster = CASTERS[type];
|
322 | var result = object;
|
323 | if (caster) {
|
324 | result = caster(object);
|
325 | }
|
326 | return result === '[object Object]' ? '' : result;
|
327 | }
|
328 |
|
329 | |
330 |
|
331 |
|
332 |
|
333 |
|
334 |
|
335 |
|
336 | type(object) {
|
337 | if (isNumber(object)) {
|
338 | return numberToBsonType(object);
|
339 | }
|
340 | if (isPlainObject(object)) {
|
341 | return OBJECT;
|
342 | }
|
343 | if (isArray(object)) {
|
344 | return ARRAY;
|
345 | }
|
346 | if (has(object, BSON_TYPE)) {
|
347 | if (object._bsontype === 'Long') {
|
348 | return 'Int64';
|
349 | }
|
350 | return object._bsontype;
|
351 | }
|
352 | return Object.prototype.toString.call(object).replace(MATCH, '$1');
|
353 | }
|
354 |
|
355 | |
356 |
|
357 |
|
358 |
|
359 |
|
360 |
|
361 |
|
362 | castableTypes(object, highPrecisionSupport = false) {
|
363 | if (isString(object)) {
|
364 | return this._stringTypes(object, highPrecisionSupport);
|
365 | } else if (isNumber(object)) {
|
366 | return this._stringTypes(String(object), highPrecisionSupport);
|
367 | } else if (has(object, BSON_TYPE) && this._isNumberType(object._bsontype)) {
|
368 | var rawValue = object._bsontype === 'Long' ? object.toNumber() : object.valueOf();
|
369 | return this._stringTypes(String(rawValue), highPrecisionSupport);
|
370 | } else if (isPlainObject(object)) {
|
371 | if (isEmpty(object)) {
|
372 | return [ 'Object', 'Array' ];
|
373 | }
|
374 | return [ 'Object' ];
|
375 | } else if (isArray(object)) {
|
376 | if (isEmpty(object)) {
|
377 | return [ 'Object', 'Array' ];
|
378 | }
|
379 | return [ 'Array' ];
|
380 | }
|
381 |
|
382 | return [ this.type(object), 'String', 'Object', 'Array' ];
|
383 | }
|
384 |
|
385 | _isNumberType(bsontype) {
|
386 | return includes(NUMBER_TYPES, bsontype);
|
387 | }
|
388 |
|
389 | _stringTypes(string, highPrecisionSupport) {
|
390 | var passing = find(highPrecisionSupport ? HP_STRING_TESTS : STRING_TESTS, (test) => {
|
391 | return test.tester.test(string);
|
392 | });
|
393 | return passing ? passing.types : [ 'String', 'Object', 'Array' ];
|
394 | }
|
395 | }
|
396 |
|
397 | module.exports = new TypeChecker();
|