UNPKG

16.2 kBJavaScriptView Raw
1function isClassConstructor(k) {
2 return !!(k.prototype && k.prototype.constructor);
3}
4function isClassObject(k) {
5 return !!(k.constructor && typeof k.constructor.name === 'string');
6}
7function compareNotFalsy(a, b) {
8 return !!a && a === b;
9}
10function getFunctionName(R) {
11 return R.toString().replace(/^function /, '').split('(')[0];
12}
13function functionToString(R) {
14 return R.toString().replace(/^.+?\{/s, '').replace(/\}.*?$/s, '').trim().replace(/[\t\n\r ]*/g, ' ');
15}
16/**
17 * https://stackoverflow.com/questions/7616461/generate-a-hash-from-string-in-javascript
18 *
19 * https://stackoverflow.com/a/52171480/9023855
20 *
21 * @param str
22 * @param seed
23 */
24function cyrb53(str, seed = 0) {
25 let h1 = 0xdeadbeef ^ seed;
26 let h2 = 0x41c6ce57 ^ seed;
27 for (let i = 0, ch; i < str.length; i++) {
28 ch = str.charCodeAt(i);
29 h1 = Math.imul(h1 ^ ch, 2654435761);
30 h2 = Math.imul(h2 ^ ch, 1597334677);
31 }
32 h1 = Math.imul(h1 ^ h1 >>> 16, 2246822507) ^ Math.imul(h2 ^ h2 >>> 13, 3266489909);
33 h2 = Math.imul(h2 ^ h2 >>> 16, 2246822507) ^ Math.imul(h1 ^ h1 >>> 13, 3266489909);
34 return 4294967296 * (2097151 & h2) + (h1 >>> 0);
35}
36/**
37 * https://stackoverflow.com/questions/34699529/convert-javascript-class-instance-to-plain-object-preserving-methods
38 */
39function extractObjectFromClass(o, exclude = []) {
40 Object.getOwnPropertyNames(o).map((prop) => {
41 const val = o[prop];
42 if (['constructor', ...exclude].includes(prop)) {
43 return;
44 }
45 });
46 return o;
47}
48
49function getTypeofDetailed(a) {
50 const typeof_ = typeof a;
51 const output = {
52 typeof_,
53 is: [],
54 entry: a
55 };
56 if (typeof_ === 'object') {
57 if (!a) {
58 output.is = ['Null'];
59 }
60 else {
61 /**
62 * constructor will return Class constructor
63 * or Object constructor for an Object
64 *
65 * The actual constructor name can be accessed via
66 * `constructor.name`
67 *
68 * constructor can checked for equality as well, for example
69 * Object === Object
70 *
71 * Not sure what happens when you `extends` Object or Array
72 * in which case, it might be better to check `constructor.name`
73 */
74 output.id = a.constructor;
75 if (output.id === Object) {
76 output.is = ['object'];
77 }
78 else if (output.id === Array) {
79 output.is = ['Array'];
80 /**
81 * Array.isArray() also includes classes that extend Array
82 * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/isArray
83 *
84 * Given a TypedArray instance, false is always returned
85 */
86 // } else if (Array.isArray(a)) {
87 // output.is = ['NamedArray']
88 }
89 else {
90 output.is = ['Named'];
91 }
92 }
93 }
94 else if (typeof_ === 'function') {
95 /**
96 * Checking for Class constructor is a difficult topic.
97 * https://stackoverflow.com/questions/40922531/how-to-check-if-a-javascript-function-is-a-constructor
98 *
99 * Probably the safest way is by checking prototype.
100 *
101 * new a() is more failsafe, but is dangerous. (because you ran a function)
102 */
103 if (!a.prototype) {
104 /**
105 * Arrow function doesn't have a prototype
106 */
107 output.is = ['function'];
108 }
109 else {
110 output.id = a.prototype.constructor;
111 if (Object.getOwnPropertyNames(a.prototype).some((el) => el !== 'constructor')) {
112 output.description = 'Can also be a class constructor in some cases';
113 if (/[A-Z]/.test(a.prototype.constructor.name[0])) {
114 output.is = ['Constructor', 'function'];
115 }
116 else {
117 output.is = ['function', 'Constructor'];
118 }
119 }
120 else {
121 output.is = ['Constructor'];
122 }
123 }
124 }
125 else if (typeof_ === 'number') {
126 if (isNaN(a)) {
127 output.is = ['NaN']; // JSON.stringify returns null
128 }
129 else if (!isFinite(a)) {
130 output.is = ['Infinity']; // JSON.stringify returns null
131 }
132 else if (Math.round(a) === a) {
133 output.description = 'Integer'; // JSON.stringify cannot distinguish, nor do JavaScript
134 }
135 }
136 if (output.is.length === 0) {
137 output.is = [typeof_];
138 }
139 return output;
140}
141function isArray(a, t) {
142 return (t || getTypeofDetailed(a)).is[0] === 'Array';
143}
144function isObject(a, t) {
145 return (t || getTypeofDetailed(a).is[0]) === 'object';
146}
147
148const MongoDateAdapter = {
149 prefix: '',
150 key: '$date',
151 item: Date,
152 fromJSON: (current) => new Date(current)
153};
154const MongoRegExpAdapter = {
155 prefix: '',
156 key: '$regex',
157 item: RegExp,
158 fromJSON(current, parent) {
159 return new RegExp(current, parent.$options);
160 },
161 toJSON(_this, parent) {
162 parent.$options = _this.flags;
163 return _this.source;
164 }
165};
166
167class Serialize {
168 constructor(
169 /**
170 * For how to write a replacer and reviver, see
171 * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON
172 */
173 options = {}) {
174 this.registrar = [];
175 this.prefix = '__';
176 this.stringifyFunction = JSON.stringify;
177 this.parseFunction = JSON.parse;
178 this.undefinedProxy = Symbol('undefined');
179 this.prefix = typeof options.prefix === 'string' ? options.prefix : this.prefix;
180 this.stringifyFunction = options.stringify || this.stringifyFunction;
181 this.parseFunction = options.parse || this.parseFunction;
182 this.register({ item: Date }, {
183 item: RegExp,
184 toJSON(_this) {
185 const { source, flags } = _this;
186 return { source, flags };
187 },
188 fromJSON(current) {
189 const { source, flags } = current;
190 return new RegExp(source, flags);
191 }
192 }, WriteOnlyFunctionAdapter, {
193 item: Set,
194 toJSON(_this) {
195 return Array.from(_this);
196 }
197 }, {
198 key: 'Infinity',
199 toJSON(_this) {
200 return _this.toString();
201 },
202 fromJSON(current) {
203 return Number(current);
204 }
205 }, {
206 key: 'bigint',
207 toJSON(_this) {
208 return _this.toString();
209 },
210 fromJSON(current) {
211 return BigInt(current);
212 }
213 }, {
214 key: 'symbol',
215 toJSON(_this) {
216 return {
217 content: _this.toString(),
218 rand: Math.random().toString(36).substr(2)
219 };
220 },
221 fromJSON({ content }) {
222 return Symbol(content.replace(/^Symbol\(/i, '').replace(/\)$/, ''));
223 }
224 }, {
225 key: 'NaN',
226 toJSON: () => 'NaN',
227 fromJSON: () => NaN
228 });
229 }
230 /**
231 *
232 * @param rs Accepts Class constructors or IRegistration
233 */
234 register(...rs) {
235 this.registrar.unshift(...rs.map((r) => {
236 if (typeof r === 'function') {
237 const { __prefix__: prefix, __key__: key, fromJSON, toJSON } = r;
238 return {
239 item: r,
240 prefix,
241 key,
242 fromJSON,
243 toJSON
244 };
245 }
246 return r;
247 }).map(({ item: R, prefix, key, toJSON, fromJSON }) => {
248 // @ts-ignore
249 fromJSON = typeof fromJSON === 'undefined'
250 ? (arg) => isClassConstructor(R) ? new R(arg) : arg
251 : (fromJSON || undefined);
252 key = this.getKey(prefix, key || (isClassConstructor(R)
253 ? R.prototype.constructor.name
254 : typeof R === 'function' ? getFunctionName(R) : R));
255 return {
256 R,
257 key,
258 toJSON,
259 fromJSON
260 };
261 }));
262 }
263 unregister(...rs) {
264 this.registrar = this.registrar.filter(({ R, key }) => {
265 return !rs.some((r) => {
266 if (typeof r === 'function') {
267 return !!R && r.constructor === R.constructor;
268 }
269 else {
270 return compareNotFalsy(r.key, key) || (!!r.item && !!R && compareNotFalsy(r.item.constructor, R.constructor));
271 }
272 });
273 });
274 }
275 /**
276 *
277 * @param obj Uses `JSON.stringify` with sorter Array by default
278 */
279 stringify(obj) {
280 const clonedObj = this.deepCloneAndFindAndReplace([obj], 'jsonCompatible')[0];
281 const keys = new Set();
282 const getAndSortKeys = (a) => {
283 if (a) {
284 if (typeof a === 'object' && a.constructor.name === 'Object') {
285 for (const k of Object.keys(a)) {
286 keys.add(k);
287 getAndSortKeys(a[k]);
288 }
289 }
290 else if (Array.isArray(a)) {
291 a.map((el) => getAndSortKeys(el));
292 }
293 }
294 };
295 getAndSortKeys(clonedObj);
296 return this.stringifyFunction(clonedObj, Array.from(keys).sort());
297 }
298 hash(obj) {
299 return cyrb53(this.stringify(obj)).toString(36);
300 }
301 clone(obj) {
302 return this.deepCloneAndFindAndReplace([obj], 'clone')[0];
303 }
304 deepEqual(o1, o2) {
305 const t1 = getTypeofDetailed(o1);
306 const t2 = getTypeofDetailed(o2);
307 if (t1.typeof_ === 'function' || t1.typeof_ === 'object') {
308 if (isArray(o1, t1)) {
309 return o1.map((el1, k) => {
310 return this.deepEqual(el1, o2[k]);
311 }).every((el) => el);
312 }
313 else if (isObject(o1, t1)) {
314 return Object.entries(o1).map(([k, el1]) => {
315 return this.deepEqual(el1, o2[k]);
316 }).every((el) => el);
317 }
318 else {
319 return this.hash(o1) === this.hash(o2);
320 }
321 }
322 else if (t1.is[0] === 'NaN' && t2.is[0] === 'NaN') {
323 /**
324 * Cannot compare directly because of infamous `NaN !== NaN`
325 */
326 return this.hash(o1) === this.hash(o2);
327 }
328 return o1 === o2;
329 }
330 /**
331 *
332 * @param repr Uses `JSON.parse` by default
333 */
334 parse(repr) {
335 return this.parseFunction(repr, (_, v) => {
336 if (v && typeof v === 'object') {
337 for (const { key, fromJSON } of this.registrar) {
338 if (v[key]) {
339 return typeof fromJSON === 'function' ? fromJSON(v[key], v) : v;
340 }
341 }
342 }
343 return v;
344 });
345 }
346 getKey(prefix, name) {
347 return (typeof prefix === 'string' ? prefix : this.prefix) + (name || '');
348 }
349 /**
350 *
351 * @param o
352 * @param type Type of findAndReplace
353 */
354 deepCloneAndFindAndReplace(o, type) {
355 const t = getTypeofDetailed(o);
356 if (t.is[0] === 'Array') {
357 const obj = [];
358 o.map((el, i) => {
359 const v = this.deepCloneAndFindAndReplace(el, type);
360 /**
361 * `undefined` can't be ignored in serialization, and will be JSON.stringify as `null`
362 */
363 if (v === this.undefinedProxy) {
364 obj[i] = undefined;
365 }
366 else {
367 obj[i] = v;
368 }
369 });
370 return obj;
371 }
372 else if (t.is[0] === 'object') {
373 const obj = {};
374 Object.entries(o).map(([k, el]) => {
375 const v = this.deepCloneAndFindAndReplace(el, type);
376 if (v === undefined) ;
377 else if (v === this.undefinedProxy) {
378 obj[k] = undefined;
379 }
380 else {
381 obj[k] = v;
382 }
383 });
384 return obj;
385 }
386 else if (t.is[0] === 'Named') {
387 const k = this.getKey(o.__prefix__, o.__name__ || o.constructor.name);
388 for (const { R, key, toJSON, fromJSON } of this.registrar) {
389 if ((!!R && compareNotFalsy(o.constructor, R)) ||
390 compareNotFalsy(k, key)) {
391 if (!fromJSON && type === 'clone') {
392 continue;
393 }
394 const p = {};
395 p[key] = ((toJSON || (!!R && R.prototype.toJSON) || o.toJSON || o.toString).bind(o))(o, p);
396 if (p[key] === undefined) {
397 return undefined;
398 }
399 else if (type === 'clone') {
400 return fromJSON(p[key], p);
401 }
402 else {
403 return p;
404 }
405 }
406 }
407 if (type === 'clone') {
408 return o;
409 }
410 else {
411 return {
412 [k]: extractObjectFromClass(o)
413 };
414 }
415 }
416 else if (t.is[0] === 'Constructor' || t.is[0] === 'function' || t.is[0] === 'Infinity' ||
417 t.is[0] === 'bigint' || t.is[0] === 'symbol' || t.is[0] === 'NaN') {
418 let is = t.is[0];
419 if (is === 'Constructor') {
420 is = 'function';
421 }
422 /**
423 * functions should be attempted to be deep-cloned first
424 * because functions are objects and can be attach properties
425 */
426 if (type === 'clone' && is !== 'function') {
427 return o;
428 }
429 const k = this.getKey(undefined, is);
430 const { R, toJSON, fromJSON } = this.registrar.filter(({ key }) => key === k)[0] || {};
431 if (type === 'clone' && !fromJSON) {
432 return o;
433 }
434 const p = {};
435 p[k] = ((toJSON || (!!R && R.prototype.toJSON) || o.toJSON || o.toString).bind(o))(o, p);
436 if (type === 'clone') {
437 return fromJSON(p[k], p);
438 }
439 else if (p[k] === undefined) {
440 return undefined;
441 }
442 else {
443 return p;
444 }
445 }
446 else if (t.is[0] === 'undefined') {
447 if (type === 'clone') {
448 return this.undefinedProxy;
449 }
450 const k = this.getKey(undefined, t.is[0]);
451 const { R, toJSON } = this.registrar.filter(({ key }) => key === k)[0] || {};
452 const p = {};
453 p[k] = ((toJSON || (!!R && R.prototype.toJSON) || (() => { })).bind(o))(o, p);
454 return p[k] === undefined ? undefined : p;
455 }
456 return o;
457 }
458}
459const FullFunctionAdapter = {
460 key: 'function',
461 toJSON: (_this) => _this.toString().trim().replace(/\[native code\]/g, ' ').replace(/[\t\n\r ]+/g, ' '),
462 fromJSON: (content) => {
463 // eslint-disable-next-line no-new-func
464 return new Function(`return ${content}`)();
465 }
466};
467const WriteOnlyFunctionAdapter = Object.assign(Object.assign({}, FullFunctionAdapter), { fromJSON: null });
468const UndefinedAdapter = {
469 key: 'undefined',
470 toJSON: () => 'undefined',
471 fromJSON: () => undefined
472};
473
474export { FullFunctionAdapter, MongoDateAdapter, MongoRegExpAdapter, Serialize, UndefinedAdapter, WriteOnlyFunctionAdapter, compareNotFalsy, cyrb53, extractObjectFromClass, functionToString, getFunctionName, isClassConstructor, isClassObject };
475//# sourceMappingURL=index.mjs.map