UNPKG

9.41 kBJavaScriptView Raw
1'use strict';
2
3const isPlainObject = require('lodash.isplainobject');
4const isArray = require('lodash.isarray');
5const isString = require('lodash.isstring');
6const isNumber = require('lodash.isnumber');
7const isEmpty = require('lodash.isempty');
8const has = require('lodash.has');
9const find = require('lodash.find');
10const toNumber = require('lodash.tonumber');
11const toString = require('lodash.tostring');
12const includes = require('lodash.includes');
13const bson = require('bson');
14const MinKey = bson.MinKey;
15const MaxKey = bson.MaxKey;
16const Long = bson.Long;
17const Double = bson.Double;
18const Int32 = bson.Int32;
19const Decimal128 = bson.Decimal128;
20
21/**
22 * The object string.
23 */
24const OBJECT = 'Object';
25
26/**
27 * The array type string.
28 */
29const ARRAY = 'Array';
30
31/**
32 * The bson type field.
33 */
34const BSON_TYPE = '_bsontype';
35
36/**
37 * The match regex.
38 */
39const MATCH = /\[object (\w+)\]/;
40
41/**
42 * The max int 32 value.
43 */
44const BSON_INT32_MAX = 0x7FFFFFFF;
45
46/**
47 * The min int 32 value.
48 */
49const BSON_INT32_MIN = -0x80000000;
50
51/**
52 * The max double value.
53 */
54const MAX_DBL = Number.MAX_SAFE_INTEGER;
55
56/**
57 * All bson types that are numbers.
58 */
59const NUMBER_TYPES = [
60 'Long',
61 'Int32',
62 'Double',
63 'Decimal128'
64];
65
66function toDate(object) {
67 return new Date(object);
68}
69
70function toMinKey() {
71 return new MinKey();
72}
73
74function toMaxKey() {
75 return new MaxKey();
76}
77
78function toUndefined() {
79 return undefined;
80}
81
82function toNull() {
83 return null;
84}
85
86function 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
99function toObject(object) {
100 if (isPlainObject(object)) {
101 return object;
102 }
103 return {};
104}
105
106function toArray(object) {
107 if (isArray(object)) {
108 return object;
109 }
110 if (isPlainObject(object)) {
111 return [];
112 }
113 return [ object ];
114}
115
116function toInt32(object) {
117 return new Int32(toNumber(object));
118}
119
120function toInt64(object) {
121 return Long.fromNumber(toNumber(object));
122}
123
124function toDouble(object) {
125 return new Double(toNumber(object));
126}
127
128function toDecimal128(object) {
129 return Decimal128.fromString(String(object));
130}
131
132/**
133 * The functions to cast to a type.
134 */
135const 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 * A test that returns the types is passing.
153 */
154class Test {
155 constructor(tester, types) {
156 this.tester = tester;
157 this.types = types;
158 }
159}
160
161const NUMBER_REGEX = /^-?\d+$/;
162
163/**
164 * Checks if a string is an int32.
165 */
166class 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 * Checks if a string is an int64.
178 */
179class 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 * Checks if integer can be cast to double.
190 */
191class 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
201const DOUBLE_REGEX = /^-?(\d*\.)?\d{1,15}$/;
202
203/**
204 * Checks if the value can be cast to a double.
205 */
206class 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
216var PARSE_STRING_REGEXP = /^(?=\d)(\+|\-)?(\d+|(\d*\.\d*))?(E|e)?([\-\+])?(\d+)?$/;
217var PARSE_INF_REGEXP = /^(\+|\-)?(Infinity|inf)$/i;
218var PARSE_NAN_REGEXP = /^(\+|\-)?NaN$/i;
219
220/**
221 * Checks if the value can be cast to a decimal 128.
222 */
223class 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
236const DATE_REGEX = /^(\d{4})-(\d|\d{2})-(\d|\d{2})(T\d{2}\:\d{2}\:\d{2}(\.\d+)?)?$/;
237
238/**
239 * Checks if a string is a date.
240 */
241class 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
250const INT32_CHECK = new Int32Check();
251const INT64_CHECK = new Int64Check();
252const INT_DBL_CHECK = new IntDblCheck();
253const DOUBLE_CHECK = new DoubleCheck();
254const DECIMAL_128_CHECK = new Decimal128Check();
255const DATE_CHECK = new DateCheck();
256
257/**
258 * The various string tests.
259 */
260const 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 * String tests with high precision support.
275 */
276const 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 * Gets the BSON type for a JS number.
292 *
293 * @param {Number} number - The number.
294 *
295 * @returns {String} The BSON type.
296 */
297function 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 * Checks the types of objects and returns them as readable strings.
309 */
310class TypeChecker {
311
312 /**
313 * Cast the provided object to the desired type.
314 *
315 * @param {Object} object - The object to cast.
316 * @param {String} type - The type.
317 *
318 * @returns {Object} The cast object.
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 * Get the type for the object.
331 *
332 * @param {Object} object - The object.
333 *
334 * @returns {String} The object type.
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 * Get a list of types the object can be cast to.
357 *
358 * @param {Object} - The object.
359 *
360 * @returns {Array} The available types.
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
397module.exports = new TypeChecker();