UNPKG

any-serialize

Version:

Serialize / Deserialize any JavaScript objects, as long as you provides how-to. I have already provided `Date`, `RegExp`, `Set` and `Function`.

476 lines (471 loc) 16.2 kB
function isClassConstructor(k) { return !!(k.prototype && k.prototype.constructor); } function isClassObject(k) { return !!(k.constructor && typeof k.constructor.name === 'string'); } function compareNotFalsy(a, b) { return !!a && a === b; } function getFunctionName(R) { return R.toString().replace(/^function /, '').split('(')[0]; } function functionToString(R) { return R.toString().replace(/^.+?\{/s, '').replace(/\}.*?$/s, '').trim().replace(/[\t\n\r ]*/g, ' '); } /** * https://stackoverflow.com/questions/7616461/generate-a-hash-from-string-in-javascript * * https://stackoverflow.com/a/52171480/9023855 * * @param str * @param seed */ function cyrb53(str, seed = 0) { let h1 = 0xdeadbeef ^ seed; let h2 = 0x41c6ce57 ^ seed; for (let i = 0, ch; i < str.length; i++) { ch = str.charCodeAt(i); h1 = Math.imul(h1 ^ ch, 2654435761); h2 = Math.imul(h2 ^ ch, 1597334677); } h1 = Math.imul(h1 ^ h1 >>> 16, 2246822507) ^ Math.imul(h2 ^ h2 >>> 13, 3266489909); h2 = Math.imul(h2 ^ h2 >>> 16, 2246822507) ^ Math.imul(h1 ^ h1 >>> 13, 3266489909); return 4294967296 * (2097151 & h2) + (h1 >>> 0); } /** * https://stackoverflow.com/questions/34699529/convert-javascript-class-instance-to-plain-object-preserving-methods */ function extractObjectFromClass(o, exclude = []) { Object.getOwnPropertyNames(o).map((prop) => { const val = o[prop]; if (['constructor', ...exclude].includes(prop)) { return; } }); return o; } function getTypeofDetailed(a) { const typeof_ = typeof a; const output = { typeof_, is: [], entry: a }; if (typeof_ === 'object') { if (!a) { output.is = ['Null']; } else { /** * constructor will return Class constructor * or Object constructor for an Object * * The actual constructor name can be accessed via * `constructor.name` * * constructor can checked for equality as well, for example * Object === Object * * Not sure what happens when you `extends` Object or Array * in which case, it might be better to check `constructor.name` */ output.id = a.constructor; if (output.id === Object) { output.is = ['object']; } else if (output.id === Array) { output.is = ['Array']; /** * Array.isArray() also includes classes that extend Array * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/isArray * * Given a TypedArray instance, false is always returned */ // } else if (Array.isArray(a)) { // output.is = ['NamedArray'] } else { output.is = ['Named']; } } } else if (typeof_ === 'function') { /** * Checking for Class constructor is a difficult topic. * https://stackoverflow.com/questions/40922531/how-to-check-if-a-javascript-function-is-a-constructor * * Probably the safest way is by checking prototype. * * new a() is more failsafe, but is dangerous. (because you ran a function) */ if (!a.prototype) { /** * Arrow function doesn't have a prototype */ output.is = ['function']; } else { output.id = a.prototype.constructor; if (Object.getOwnPropertyNames(a.prototype).some((el) => el !== 'constructor')) { output.description = 'Can also be a class constructor in some cases'; if (/[A-Z]/.test(a.prototype.constructor.name[0])) { output.is = ['Constructor', 'function']; } else { output.is = ['function', 'Constructor']; } } else { output.is = ['Constructor']; } } } else if (typeof_ === 'number') { if (isNaN(a)) { output.is = ['NaN']; // JSON.stringify returns null } else if (!isFinite(a)) { output.is = ['Infinity']; // JSON.stringify returns null } else if (Math.round(a) === a) { output.description = 'Integer'; // JSON.stringify cannot distinguish, nor do JavaScript } } if (output.is.length === 0) { output.is = [typeof_]; } return output; } function isArray(a, t) { return (t || getTypeofDetailed(a)).is[0] === 'Array'; } function isObject(a, t) { return (t || getTypeofDetailed(a).is[0]) === 'object'; } const MongoDateAdapter = { prefix: '', key: '$date', item: Date, fromJSON: (current) => new Date(current) }; const MongoRegExpAdapter = { prefix: '', key: '$regex', item: RegExp, fromJSON(current, parent) { return new RegExp(current, parent.$options); }, toJSON(_this, parent) { parent.$options = _this.flags; return _this.source; } }; class Serialize { constructor( /** * For how to write a replacer and reviver, see * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON */ options = {}) { this.registrar = []; this.prefix = '__'; this.stringifyFunction = JSON.stringify; this.parseFunction = JSON.parse; this.undefinedProxy = Symbol('undefined'); this.prefix = typeof options.prefix === 'string' ? options.prefix : this.prefix; this.stringifyFunction = options.stringify || this.stringifyFunction; this.parseFunction = options.parse || this.parseFunction; this.register({ item: Date }, { item: RegExp, toJSON(_this) { const { source, flags } = _this; return { source, flags }; }, fromJSON(current) { const { source, flags } = current; return new RegExp(source, flags); } }, WriteOnlyFunctionAdapter, { item: Set, toJSON(_this) { return Array.from(_this); } }, { key: 'Infinity', toJSON(_this) { return _this.toString(); }, fromJSON(current) { return Number(current); } }, { key: 'bigint', toJSON(_this) { return _this.toString(); }, fromJSON(current) { return BigInt(current); } }, { key: 'symbol', toJSON(_this) { return { content: _this.toString(), rand: Math.random().toString(36).substr(2) }; }, fromJSON({ content }) { return Symbol(content.replace(/^Symbol\(/i, '').replace(/\)$/, '')); } }, { key: 'NaN', toJSON: () => 'NaN', fromJSON: () => NaN }); } /** * * @param rs Accepts Class constructors or IRegistration */ register(...rs) { this.registrar.unshift(...rs.map((r) => { if (typeof r === 'function') { const { __prefix__: prefix, __key__: key, fromJSON, toJSON } = r; return { item: r, prefix, key, fromJSON, toJSON }; } return r; }).map(({ item: R, prefix, key, toJSON, fromJSON }) => { // @ts-ignore fromJSON = typeof fromJSON === 'undefined' ? (arg) => isClassConstructor(R) ? new R(arg) : arg : (fromJSON || undefined); key = this.getKey(prefix, key || (isClassConstructor(R) ? R.prototype.constructor.name : typeof R === 'function' ? getFunctionName(R) : R)); return { R, key, toJSON, fromJSON }; })); } unregister(...rs) { this.registrar = this.registrar.filter(({ R, key }) => { return !rs.some((r) => { if (typeof r === 'function') { return !!R && r.constructor === R.constructor; } else { return compareNotFalsy(r.key, key) || (!!r.item && !!R && compareNotFalsy(r.item.constructor, R.constructor)); } }); }); } /** * * @param obj Uses `JSON.stringify` with sorter Array by default */ stringify(obj) { const clonedObj = this.deepCloneAndFindAndReplace([obj], 'jsonCompatible')[0]; const keys = new Set(); const getAndSortKeys = (a) => { if (a) { if (typeof a === 'object' && a.constructor.name === 'Object') { for (const k of Object.keys(a)) { keys.add(k); getAndSortKeys(a[k]); } } else if (Array.isArray(a)) { a.map((el) => getAndSortKeys(el)); } } }; getAndSortKeys(clonedObj); return this.stringifyFunction(clonedObj, Array.from(keys).sort()); } hash(obj) { return cyrb53(this.stringify(obj)).toString(36); } clone(obj) { return this.deepCloneAndFindAndReplace([obj], 'clone')[0]; } deepEqual(o1, o2) { const t1 = getTypeofDetailed(o1); const t2 = getTypeofDetailed(o2); if (t1.typeof_ === 'function' || t1.typeof_ === 'object') { if (isArray(o1, t1)) { return o1.map((el1, k) => { return this.deepEqual(el1, o2[k]); }).every((el) => el); } else if (isObject(o1, t1)) { return Object.entries(o1).map(([k, el1]) => { return this.deepEqual(el1, o2[k]); }).every((el) => el); } else { return this.hash(o1) === this.hash(o2); } } else if (t1.is[0] === 'NaN' && t2.is[0] === 'NaN') { /** * Cannot compare directly because of infamous `NaN !== NaN` */ return this.hash(o1) === this.hash(o2); } return o1 === o2; } /** * * @param repr Uses `JSON.parse` by default */ parse(repr) { return this.parseFunction(repr, (_, v) => { if (v && typeof v === 'object') { for (const { key, fromJSON } of this.registrar) { if (v[key]) { return typeof fromJSON === 'function' ? fromJSON(v[key], v) : v; } } } return v; }); } getKey(prefix, name) { return (typeof prefix === 'string' ? prefix : this.prefix) + (name || ''); } /** * * @param o * @param type Type of findAndReplace */ deepCloneAndFindAndReplace(o, type) { const t = getTypeofDetailed(o); if (t.is[0] === 'Array') { const obj = []; o.map((el, i) => { const v = this.deepCloneAndFindAndReplace(el, type); /** * `undefined` can't be ignored in serialization, and will be JSON.stringify as `null` */ if (v === this.undefinedProxy) { obj[i] = undefined; } else { obj[i] = v; } }); return obj; } else if (t.is[0] === 'object') { const obj = {}; Object.entries(o).map(([k, el]) => { const v = this.deepCloneAndFindAndReplace(el, type); if (v === undefined) ; else if (v === this.undefinedProxy) { obj[k] = undefined; } else { obj[k] = v; } }); return obj; } else if (t.is[0] === 'Named') { const k = this.getKey(o.__prefix__, o.__name__ || o.constructor.name); for (const { R, key, toJSON, fromJSON } of this.registrar) { if ((!!R && compareNotFalsy(o.constructor, R)) || compareNotFalsy(k, key)) { if (!fromJSON && type === 'clone') { continue; } const p = {}; p[key] = ((toJSON || (!!R && R.prototype.toJSON) || o.toJSON || o.toString).bind(o))(o, p); if (p[key] === undefined) { return undefined; } else if (type === 'clone') { return fromJSON(p[key], p); } else { return p; } } } if (type === 'clone') { return o; } else { return { [k]: extractObjectFromClass(o) }; } } else if (t.is[0] === 'Constructor' || t.is[0] === 'function' || t.is[0] === 'Infinity' || t.is[0] === 'bigint' || t.is[0] === 'symbol' || t.is[0] === 'NaN') { let is = t.is[0]; if (is === 'Constructor') { is = 'function'; } /** * functions should be attempted to be deep-cloned first * because functions are objects and can be attach properties */ if (type === 'clone' && is !== 'function') { return o; } const k = this.getKey(undefined, is); const { R, toJSON, fromJSON } = this.registrar.filter(({ key }) => key === k)[0] || {}; if (type === 'clone' && !fromJSON) { return o; } const p = {}; p[k] = ((toJSON || (!!R && R.prototype.toJSON) || o.toJSON || o.toString).bind(o))(o, p); if (type === 'clone') { return fromJSON(p[k], p); } else if (p[k] === undefined) { return undefined; } else { return p; } } else if (t.is[0] === 'undefined') { if (type === 'clone') { return this.undefinedProxy; } const k = this.getKey(undefined, t.is[0]); const { R, toJSON } = this.registrar.filter(({ key }) => key === k)[0] || {}; const p = {}; p[k] = ((toJSON || (!!R && R.prototype.toJSON) || (() => { })).bind(o))(o, p); return p[k] === undefined ? undefined : p; } return o; } } const FullFunctionAdapter = { key: 'function', toJSON: (_this) => _this.toString().trim().replace(/\[native code\]/g, ' ').replace(/[\t\n\r ]+/g, ' '), fromJSON: (content) => { // eslint-disable-next-line no-new-func return new Function(`return ${content}`)(); } }; const WriteOnlyFunctionAdapter = Object.assign(Object.assign({}, FullFunctionAdapter), { fromJSON: null }); const UndefinedAdapter = { key: 'undefined', toJSON: () => 'undefined', fromJSON: () => undefined }; export { FullFunctionAdapter, MongoDateAdapter, MongoRegExpAdapter, Serialize, UndefinedAdapter, WriteOnlyFunctionAdapter, compareNotFalsy, cyrb53, extractObjectFromClass, functionToString, getFunctionName, isClassConstructor, isClassObject }; //# sourceMappingURL=index.mjs.map