shaochuancs/esserializer

View on GitHub
src/utils/deserializer.ts

Summary

Maintainability
A
55 mins
Test Coverage
/**
 * Created by cshao on 2021-02-19.
 */


'use strict';

import {
  getValueFromToStringResult,
  isClass,
  notObject
} from './general';
import {
  ESSERIALIZER_NULL,
  BOOLEAN_FIELD,
  BUILTIN_CLASS_INT8ARRAY,
  BUILTIN_CLASS_UINT8ARRAY,
  BUILTIN_CLASS_UINT8CLAMPEDARRAY,
  BUILTIN_CLASS_INT16ARRAY,
  BUILTIN_CLASS_UINT16ARRAY,
  BUILTIN_CLASS_INT32ARRAY,
  BUILTIN_CLASS_UINT32ARRAY,
  BUILTIN_CLASS_FLOAT32ARRAY,
  BUILTIN_CLASS_FLOAT64ARRAY,
  BUILTIN_CLASS_BIGINT64ARRAY,
  BUILTIN_CLASS_BIGUINT64ARRAY,
  BUILTIN_CLASS_ARRAYBUFFER,
  BUILTIN_CLASS_SHAREDARRAYBUFFER,
  BUILTIN_CLASS_BOOLEAN,
  BUILTIN_CLASS_DATAVIEW,
  BUILTIN_CLASS_DATE,
  BUILTIN_CLASS_INTL_COLLATOR,
  BUILTIN_CLASS_INTL_DATETIMEFORMAT,
  BUILTIN_CLASS_INTL_LISTFORMAT,
  BUILTIN_CLASS_INTL_LOCALE,
  BUILTIN_CLASS_INTL_NUMBERFORMAT,
  BUILTIN_CLASS_INTL_PLURALRULES,
  BUILTIN_CLASS_INTL_RELATIVETIMEFORMAT,
  BUILTIN_CLASS_REGEXP,
  BUILTIN_CLASS_SET,
  BUILTIN_CLASS_STRING,
  BUILTIN_CLASS_ERROR,
  BUILTIN_CLASS_EVAL_ERROR,
  BUILTIN_CLASS_RANGE_ERROR,
  BUILTIN_CLASS_REFERENCE_ERROR,
  BUILTIN_CLASS_SYNTAX_ERROR,
  BUILTIN_CLASS_TYPE_ERROR,
  BUILTIN_CLASS_URI_ERROR,
  BUILTIN_CLASS_AGGREGATE_ERROR,
  BUILTIN_TYPE_NOT_FINITE,
  BUILTIN_TYPE_UNDEFINED,
  CLASS_NAME_FIELD,
  TIMESTAMP_FIELD,
  TO_STRING_FIELD,
  BUILTIN_TYPE_BIG_INT,
  ARRAY_FIELD,
  OPTIONS_FIELD
} from './constant';
import DeserializeOptions from "../model/DeserializationOptions";
const REGEXP_BEGIN_WITH_CLASS = /^\s*class\s+/;

function deserializeFromParsedObj(parsedObj:any, classes:Array<any>, options:DeserializeOptions): any {
  return deserializeFromParsedObjWithClassMapping(parsedObj, getClassMappingFromClassArray(classes), options);
}

function deserializeFromParsedObjWithClassMapping(parsedObj:any, classMapping:object, options:DeserializeOptions={}): any {
  if (notObject(parsedObj)) {
    return parsedObj;
  }

  if (Array.isArray(parsedObj)) {
    return _deserializeArray(parsedObj, classMapping);
  }

  const classNameInParsedObj:string = parsedObj[CLASS_NAME_FIELD];
  const deserializedValueForBuiltinType = _deserializeBuiltinTypes(classNameInParsedObj, parsedObj, classMapping);
  if (deserializedValueForBuiltinType !== ESSERIALIZER_NULL) {
    return deserializedValueForBuiltinType;
  }

  if (classNameInParsedObj && !classMapping[classNameInParsedObj]) {
    throw new Error(`Class ${classNameInParsedObj} not found`);
  }

  let constructorParameters = [];
  if (options.fieldsForConstructorParameters) {
    constructorParameters = options.fieldsForConstructorParameters.map((field)=>{
      if (field in parsedObj) {
        return parsedObj[field];
      }
      return {}; // Prevent passing undefined to constructor
    });
  }

  const deserializedObj:object = deserializeClassProperty(classMapping[classNameInParsedObj], constructorParameters);
  return deserializeValuesWithClassMapping(deserializedObj, parsedObj, classMapping, options);
}

function _deserializeArray(parsedObj, classMapping:object) {
  return parsedObj.map((item) => {
    return deserializeFromParsedObjWithClassMapping(item, classMapping)
  });
}

function _deserializeBuiltinTypes(classNameInParsedObj, parsedObj, classMapping) {
  switch (classNameInParsedObj) {
    case BUILTIN_CLASS_INT8ARRAY:
      return _deserializeArrayInstance(parsedObj[ARRAY_FIELD], Int8Array);
    case BUILTIN_CLASS_UINT8ARRAY:
      return _deserializeArrayInstance(parsedObj[ARRAY_FIELD], Uint8Array);
    case BUILTIN_CLASS_UINT8CLAMPEDARRAY:
      return _deserializeArrayInstance(parsedObj[ARRAY_FIELD], Uint8ClampedArray);
    case BUILTIN_CLASS_INT16ARRAY:
      return _deserializeArrayInstance(parsedObj[ARRAY_FIELD], Int16Array);
    case BUILTIN_CLASS_UINT16ARRAY:
      return _deserializeArrayInstance(parsedObj[ARRAY_FIELD], Uint16Array);
    case BUILTIN_CLASS_INT32ARRAY:
      return _deserializeArrayInstance(parsedObj[ARRAY_FIELD], Int32Array);
    case BUILTIN_CLASS_UINT32ARRAY:
      return _deserializeArrayInstance(parsedObj[ARRAY_FIELD], Uint32Array);
    case BUILTIN_CLASS_FLOAT32ARRAY:
      return _deserializeArrayInstance(parsedObj[ARRAY_FIELD], Float32Array);
    case BUILTIN_CLASS_FLOAT64ARRAY:
      return _deserializeArrayInstance(parsedObj[ARRAY_FIELD], Float64Array);
    case BUILTIN_CLASS_BIGINT64ARRAY:
      return _deserializeBigIntArrayInstance(parsedObj[ARRAY_FIELD], BigInt64Array);
    case BUILTIN_CLASS_BIGUINT64ARRAY:
      return _deserializeBigIntArrayInstance(parsedObj[ARRAY_FIELD], BigUint64Array);
    case BUILTIN_TYPE_BIG_INT:
      return deserializeBigInt(parsedObj[TO_STRING_FIELD]);
    case BUILTIN_TYPE_UNDEFINED:
      return undefined;
    case BUILTIN_TYPE_NOT_FINITE:
      return getValueFromToStringResult(parsedObj[TO_STRING_FIELD]);
    case BUILTIN_CLASS_ARRAYBUFFER:
      return _deserializeArrayBuffer(parsedObj[ARRAY_FIELD]);
    case BUILTIN_CLASS_SHAREDARRAYBUFFER:
      return _deserializeSharedArrayBuffer(parsedObj[ARRAY_FIELD]);
    case BUILTIN_CLASS_BOOLEAN:
      return deserializeBoolean(parsedObj);
    case BUILTIN_CLASS_DATAVIEW:
      return _deserializeDataView(parsedObj[ARRAY_FIELD]);
    case BUILTIN_CLASS_DATE:
      return deserializeDate(parsedObj);
    case BUILTIN_CLASS_INTL_COLLATOR:
      return _deserializeIntlInstance(parsedObj, Intl.Collator);
    case BUILTIN_CLASS_INTL_DATETIMEFORMAT:
      return _deserializeIntlInstance(parsedObj, Intl.DateTimeFormat);
    case BUILTIN_CLASS_INTL_LISTFORMAT:
      // @ts-ignore
      return _deserializeIntlInstance(parsedObj, Intl.ListFormat);
    case BUILTIN_CLASS_INTL_LOCALE:
      // @ts-ignore
      return new Intl.Locale(parsedObj[TO_STRING_FIELD]);
    case BUILTIN_CLASS_INTL_NUMBERFORMAT:
      return _deserializeIntlInstance(parsedObj, Intl.NumberFormat);
    case BUILTIN_CLASS_INTL_PLURALRULES:
      return _deserializeIntlInstance(parsedObj, Intl.PluralRules);
    case BUILTIN_CLASS_INTL_RELATIVETIMEFORMAT:
      // @ts-ignore
      return _deserializeIntlInstance(parsedObj, Intl.RelativeTimeFormat);
    case BUILTIN_CLASS_REGEXP:
      return deserializeRegExp(parsedObj);
    case BUILTIN_CLASS_SET:
      return _deserializeSet(parsedObj, classMapping);
    case BUILTIN_CLASS_STRING:
      return deserializeString(parsedObj);
    case BUILTIN_CLASS_ERROR:
      return deserializeError(parsedObj, Error);
    case BUILTIN_CLASS_EVAL_ERROR:
      return deserializeError(parsedObj, EvalError);
    case BUILTIN_CLASS_RANGE_ERROR:
      return deserializeError(parsedObj, RangeError);
    case BUILTIN_CLASS_REFERENCE_ERROR:
      return deserializeError(parsedObj, ReferenceError);
    case BUILTIN_CLASS_SYNTAX_ERROR:
      return deserializeError(parsedObj, SyntaxError);
    case BUILTIN_CLASS_TYPE_ERROR:
      return deserializeError(parsedObj, TypeError);
    case BUILTIN_CLASS_URI_ERROR:
      return deserializeError(parsedObj, URIError);
    case BUILTIN_CLASS_AGGREGATE_ERROR:
      return deserializeError(parsedObj, AggregateError);
    default:
      return ESSERIALIZER_NULL;
  }
}

function _deserializeArrayBuffer(byteArray) {
  return new Uint8Array(byteArray).buffer;
}

function _deserializeSharedArrayBuffer(byteArray) {
  const sab = new SharedArrayBuffer(byteArray.length);
  const sabViewArray = new Uint8Array(sab);
  byteArray.forEach((byte, index) => {
    sabViewArray[index] = byte;
  });
  return sab;
}

function _deserializeDataView(byteArray) {
  return new DataView(new Uint8Array(byteArray).buffer);
}

function _deserializeArrayInstance(arr, ArrayClass) {
  return new ArrayClass(arr);
}

function _deserializeBigIntArrayInstance(arr, ArrayClass) {
  return new ArrayClass(arr.map((biObj) => {
    return deserializeBigInt(biObj[TO_STRING_FIELD]);
  }));
}

function deserializeBigInt(str) {
  return BigInt(str);
}

function deserializeBoolean(parsedObj) {
  return new Boolean(parsedObj[BOOLEAN_FIELD]);
}

function deserializeDate(parsedObj) {
  return typeof parsedObj[TIMESTAMP_FIELD] === 'number' ? new Date(parsedObj[TIMESTAMP_FIELD]) : null;
}

function _deserializeIntlInstance(parsedObj, IntlClass) {
  const options = parsedObj[OPTIONS_FIELD];
  const locale = options.locale;
  delete options.locale;
  return new IntlClass(locale, options);
}

function deserializeRegExp(parsedObj) {
  const regExpStr = parsedObj[TO_STRING_FIELD];
  const lastIndexOfSlash = regExpStr.lastIndexOf('/');
  return new RegExp(regExpStr.substring(1, lastIndexOfSlash), regExpStr.substring(lastIndexOfSlash+1));
}

function _deserializeSet(parsedObj, classMapping) {
  return new Set(_deserializeArray(parsedObj[ARRAY_FIELD], classMapping));
}

function deserializeString(parsedObj) {
  return new String(parsedObj[TO_STRING_FIELD]);
}

function deserializeError(parsedObj, ErrorClass) {
  let error;
  if (parsedObj.message) {
    // @ts-ignore
    error = new ErrorClass(parsedObj.message);
  } else {
    // @ts-ignore
    error = new ErrorClass();
  }
  delete error.stack;

  if (parsedObj.name) {
    error.name = parsedObj.name;
  }
  if (parsedObj.stack) {
    error.stack = parsedObj.stack;
  }

  if (ErrorClass === AggregateError) {
    error.errors = deserializeFromParsedObjWithClassMapping(parsedObj.errors, {});
  }

  return error;
}

function deserializeClassProperty(classObj, constructorParameters:Array<any>) {
  if (!classObj) {
    return {};
  }

  const additionalParameterNumber = Math.max(classObj.length - constructorParameters.length, 0);
  const FALLBACK_PARAMETERS = [{}, 0, '', null];
  let fallbackParam = FALLBACK_PARAMETERS.shift();
  let deserializedObj;
  while (!deserializedObj && fallbackParam !== undefined) {
    deserializedObj = createDeserializedObj(
      classObj,
      constructorParameters.concat(Array.from(Array(additionalParameterNumber), () => (fallbackParam)))
    );

    if (additionalParameterNumber === 0) {
      fallbackParam = undefined;
    } else {
      fallbackParam = FALLBACK_PARAMETERS.shift();
    }
  }
  if (!deserializedObj) {
    deserializedObj = {};
    Object.setPrototypeOf(deserializedObj, classObj ? classObj.prototype : Object.prototype);
  }

  return deserializedObj;
}

function createDeserializedObj(classObj, allConstructorParameters) {
  let deserializedObj;
  try {
    if (REGEXP_BEGIN_WITH_CLASS.test(classObj.toString())) {
      deserializedObj = new classObj(...allConstructorParameters);
    } else {// It's class in function style.
      let objectConstructor = classObj.prototype.constructor;
      if (objectConstructor.name === 'Object') {
        objectConstructor = classObj; // In case object's constructor is not in its prototype, such as Big in big.js
      }
      deserializedObj = Object.create(objectConstructor.prototype);
      objectConstructor.call(deserializedObj, allConstructorParameters)
    }
  } catch (e) {
    deserializedObj = null;
  }
  return deserializedObj;
}

function deserializeValuesWithClassMapping(deserializedObj, parsedObj, classMapping, options:DeserializeOptions) {
  for (const k in parsedObj) {
    const v = parsedObj[k];

    if (options.ignoreProperties && options.ignoreProperties.includes(k)) {
      continue;
    }
    if (options.rawProperties && options.rawProperties.includes(k)) {
      deserializedObj[k] = JSON.stringify(v);
      continue;
    }

    const descriptor = Object.getOwnPropertyDescriptor(deserializedObj, k);
    if (canSkipCopyingValue(k, v, descriptor)) {
      continue;
    }
    if (descriptor && descriptor.writable === false && typeof v === 'object') {
      assignWritableField(deserializedObj[k], v, classMapping);
      continue;
    }

    deserializedObj[k] = deserializeFromParsedObjWithClassMapping(v, classMapping);
  }
  return deserializedObj;
}

function canSkipCopyingValue(keyOfParsedObj, valueOfParsedObj, descriptorOfDeserializedObjProperty) {
  if (keyOfParsedObj === CLASS_NAME_FIELD) {
    return true;
  }
  return descriptorOfDeserializedObjProperty && (
    typeof descriptorOfDeserializedObjProperty.set === 'function' ||
    (descriptorOfDeserializedObjProperty.writable === false && typeof valueOfParsedObj !== 'object')
  );
}

function assignWritableField(targetObj, sourceObj, classMapping) {
  for (const field in sourceObj) {
    const descriptor = Object.getOwnPropertyDescriptor(targetObj, field);
    if (descriptor && !(descriptor.writable===true || typeof descriptor.set === 'function')) {
      continue;
    }
    targetObj[field] = deserializeFromParsedObjWithClassMapping(sourceObj[field], classMapping);
  }
}

/**
 *
 * @param classes It's an array of Class definition. "any" is used in code only
 * because there is no TypeScript type definition for Class.
 */
function getClassMappingFromClassArray(classes:Array<any> = []): object {
  const classMapping:object = {};
  classes.forEach((c) => {
    if (!isClass(c)) {
      return;
    }
    const className:string = c.name;
    const previousClass = classMapping[className];
    if (previousClass && previousClass !== c) {
      console.warn('WARNING: Found class definition with the same name: ' + className);
    }
    // @ts-ignore
    classMapping[className] = c;
  });

  return classMapping;
}

/**
 *
 * @param classObj It's a Class definition. "any" is used in code only
 * because there is no TypeScript type definition for Class.
 */
function getParentClassName(classObj:any): string {
  return classObj.prototype.__proto__.constructor.name;
}

export {
  deserializeFromParsedObj,
  deserializeFromParsedObjWithClassMapping,
  getClassMappingFromClassArray,
  getParentClassName
};